feat(utils): adapt WAGO CC100 hardware module
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixUtils\CC100;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
final class CC100ChannelModel
|
||||
{
|
||||
/** @var array<string, int> */
|
||||
private const CHANNEL_COUNTS = [
|
||||
'DI' => 8,
|
||||
'DO' => 4,
|
||||
'AI' => 2,
|
||||
'AO' => 2,
|
||||
'PT' => 2,
|
||||
];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private const DEFAULT_INTERVALS = [
|
||||
'DI' => 500,
|
||||
'DO' => 500,
|
||||
'AI' => 5000,
|
||||
'AO' => 5000,
|
||||
'PT' => 5000,
|
||||
];
|
||||
|
||||
/**
|
||||
* @return list<array{Aktiv: bool, Typ: string, Kanal: int, Anzeigename: string, Abfrageintervall: int}>
|
||||
*/
|
||||
public static function defaultSelection(): array
|
||||
{
|
||||
$selection = [];
|
||||
foreach (self::CHANNEL_COUNTS as $type => $count) {
|
||||
for ($channel = 1; $channel <= $count; $channel++) {
|
||||
$selection[] = [
|
||||
'Aktiv' => false,
|
||||
'Typ' => $type,
|
||||
'Kanal' => $channel,
|
||||
'Anzeigename' => self::defaultName($type, $channel),
|
||||
'Abfrageintervall' => self::DEFAULT_INTERVALS[$type],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $selection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{
|
||||
* key: string,
|
||||
* type: string,
|
||||
* channel: int,
|
||||
* name: string,
|
||||
* active: bool,
|
||||
* pollInterval: int
|
||||
* }>
|
||||
*/
|
||||
public static function normalizeSelection(string $json): array
|
||||
{
|
||||
try {
|
||||
$rows = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $exception) {
|
||||
throw new InvalidArgumentException('Kanalauswahl ist kein gueltiges JSON.', 0, $exception);
|
||||
}
|
||||
|
||||
if (!is_array($rows)) {
|
||||
throw new InvalidArgumentException('Kanalauswahl muss eine Liste sein.');
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$keys = [];
|
||||
foreach ($rows as $index => $row) {
|
||||
if (!is_array($row)) {
|
||||
throw new InvalidArgumentException('Kanalzeile ' . ($index + 1) . ' ist ungueltig.');
|
||||
}
|
||||
|
||||
$type = strtoupper(trim((string) ($row['Typ'] ?? '')));
|
||||
$channel = filter_var($row['Kanal'] ?? null, FILTER_VALIDATE_INT);
|
||||
if ($channel === false || !isset(self::CHANNEL_COUNTS[$type])) {
|
||||
throw new InvalidArgumentException('Kanalzeile ' . ($index + 1) . ' hat einen ungueltigen Typ oder Kanal.');
|
||||
}
|
||||
if ($channel < 1 || $channel > self::CHANNEL_COUNTS[$type]) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('%s erlaubt nur die Kanaele 1 bis %d.', $type, self::CHANNEL_COUNTS[$type])
|
||||
);
|
||||
}
|
||||
|
||||
$key = $type . $channel;
|
||||
if (isset($keys[$key])) {
|
||||
throw new InvalidArgumentException('Kanal ' . $key . ' ist doppelt konfiguriert.');
|
||||
}
|
||||
$keys[$key] = true;
|
||||
|
||||
$name = trim((string) ($row['Anzeigename'] ?? ''));
|
||||
if ($name === '') {
|
||||
$name = self::defaultName($type, $channel);
|
||||
}
|
||||
if (strlen($name) > 80) {
|
||||
throw new InvalidArgumentException('Der Anzeigename von ' . $key . ' ist zu lang.');
|
||||
}
|
||||
|
||||
$pollInterval = filter_var($row['Abfrageintervall'] ?? null, FILTER_VALIDATE_INT);
|
||||
if ($pollInterval === false || $pollInterval < 100 || $pollInterval > 3600000) {
|
||||
throw new InvalidArgumentException(
|
||||
'Das Abfrageintervall von ' . $key . ' muss zwischen 100 und 3600000 ms liegen.'
|
||||
);
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'key' => $key,
|
||||
'type' => $type,
|
||||
'channel' => $channel,
|
||||
'name' => $name,
|
||||
'active' => (bool) ($row['Aktiv'] ?? false),
|
||||
'pollInterval' => $pollInterval,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{variableType: int, profile: string, writable: bool, position: int}
|
||||
*/
|
||||
public static function definition(string $type, int $channel): array
|
||||
{
|
||||
$type = strtoupper($type);
|
||||
if (!isset(self::CHANNEL_COUNTS[$type]) || $channel < 1 || $channel > self::CHANNEL_COUNTS[$type]) {
|
||||
throw new InvalidArgumentException('Unbekannter CC100-Kanal.');
|
||||
}
|
||||
|
||||
$offsets = ['DI' => 100, 'DO' => 200, 'AI' => 300, 'AO' => 400, 'PT' => 500];
|
||||
|
||||
return [
|
||||
'variableType' => in_array($type, ['DI', 'DO'], true) ? 0 : 2,
|
||||
'profile' => match ($type) {
|
||||
'DO' => '~Switch',
|
||||
'AI', 'AO' => '~Volt',
|
||||
'PT' => '~Temperature',
|
||||
default => '',
|
||||
},
|
||||
'writable' => in_array($type, ['DO', 'AO'], true),
|
||||
'position' => $offsets[$type] + $channel,
|
||||
];
|
||||
}
|
||||
|
||||
public static function defaultName(string $type, int $channel): string
|
||||
{
|
||||
return match ($type) {
|
||||
'DI' => 'Digitaler Eingang ' . $channel,
|
||||
'DO' => 'Digitaler Ausgang ' . $channel,
|
||||
'AI' => 'Analoger Eingang ' . $channel,
|
||||
'AO' => 'Analoger Ausgang ' . $channel,
|
||||
'PT' => 'PT1000 Temperatur ' . $channel,
|
||||
default => $type . $channel,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{x1: float, y1: float, x2: float, y2: float}>
|
||||
*/
|
||||
public static function parseCalibration(string $content): array
|
||||
{
|
||||
$lines = preg_split('/\R/', trim($content)) ?: [];
|
||||
$lines = array_values(array_filter(array_map('trim', $lines), static fn (string $line): bool => $line !== ''));
|
||||
if (count($lines) < 2) {
|
||||
throw new InvalidArgumentException('Kalibrierdatei ist leer oder unvollstaendig.');
|
||||
}
|
||||
|
||||
$headers = preg_split('/\s+/', array_shift($lines)) ?: [];
|
||||
$calibration = [];
|
||||
foreach ($headers as $index => $header) {
|
||||
if (!isset($lines[$index])) {
|
||||
throw new InvalidArgumentException('Kalibrierwert fuer ' . $header . ' fehlt.');
|
||||
}
|
||||
|
||||
$key = strtoupper($header);
|
||||
if ($key === 'A01') {
|
||||
$key = 'AO1';
|
||||
} elseif ($key === 'A02') {
|
||||
$key = 'AO2';
|
||||
}
|
||||
|
||||
$values = preg_split('/\s+/', $lines[$index]) ?: [];
|
||||
if (count($values) !== 4 || count(array_filter($values, 'is_numeric')) !== 4) {
|
||||
throw new InvalidArgumentException('Kalibrierwert fuer ' . $key . ' ist ungueltig.');
|
||||
}
|
||||
|
||||
[$x1, $y1, $x2, $y2] = array_map('floatval', $values);
|
||||
if ($x1 === $x2 || $y1 === $y2) {
|
||||
throw new InvalidArgumentException('Kalibrierpunkte fuer ' . $key . ' sind nicht eindeutig.');
|
||||
}
|
||||
$calibration[$key] = compact('x1', 'y1', 'x2', 'y2');
|
||||
}
|
||||
|
||||
return $calibration;
|
||||
}
|
||||
|
||||
/** @param array{x1: float, y1: float, x2: float, y2: float} $calibration */
|
||||
public static function rawToEngineering(float $raw, array $calibration): float
|
||||
{
|
||||
return (($calibration['y2'] - $calibration['y1']) / ($calibration['x2'] - $calibration['x1']))
|
||||
* ($raw - $calibration['x1'])
|
||||
+ $calibration['y1'];
|
||||
}
|
||||
|
||||
/** @param array{x1: float, y1: float, x2: float, y2: float} $calibration */
|
||||
public static function engineeringToRaw(float $value, array $calibration): int
|
||||
{
|
||||
$raw = (($calibration['x2'] - $calibration['x1']) / ($calibration['y2'] - $calibration['y1']))
|
||||
* ($value - $calibration['y1'])
|
||||
+ $calibration['x1'];
|
||||
|
||||
return (int) round($raw);
|
||||
}
|
||||
|
||||
public static function pt1000Temperature(float $resistance): float
|
||||
{
|
||||
if (!is_finite($resistance) || $resistance < 760.0 || $resistance > 2300.0) {
|
||||
throw new InvalidArgumentException('PT1000-Widerstand liegt ausserhalb des unterstuetzten Bereichs.');
|
||||
}
|
||||
|
||||
$low = -60.0;
|
||||
$high = 350.0;
|
||||
for ($iteration = 0; $iteration < 50; $iteration++) {
|
||||
$middle = ($low + $high) / 2.0;
|
||||
if (self::pt1000Resistance($middle) < $resistance) {
|
||||
$low = $middle;
|
||||
} else {
|
||||
$high = $middle;
|
||||
}
|
||||
}
|
||||
|
||||
return round(($low + $high) / 2.0, 1);
|
||||
}
|
||||
|
||||
public static function parseInteger(string $content): int
|
||||
{
|
||||
$value = trim($content);
|
||||
if (!preg_match('/^-?\d+$/', $value)) {
|
||||
throw new RuntimeException('Hardwarewert ist keine ganze Zahl.');
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
public static function bitState(int $aggregate, int $channel): bool
|
||||
{
|
||||
if ($channel < 1 || $channel > 8) {
|
||||
throw new InvalidArgumentException('Bitkanal muss zwischen 1 und 8 liegen.');
|
||||
}
|
||||
|
||||
return ($aggregate & (1 << ($channel - 1))) !== 0;
|
||||
}
|
||||
|
||||
public static function withBitState(int $aggregate, int $channel, bool $state): int
|
||||
{
|
||||
$mask = 1 << ($channel - 1);
|
||||
return $state ? ($aggregate | $mask) : ($aggregate & ~$mask);
|
||||
}
|
||||
|
||||
private static function pt1000Resistance(float $temperature): float
|
||||
{
|
||||
$a = 3.9083e-3;
|
||||
$b = -5.775e-7;
|
||||
$c = -4.183e-12;
|
||||
$factor = 1.0 + ($a * $temperature) + ($b * $temperature * $temperature);
|
||||
if ($temperature < 0.0) {
|
||||
$factor += $c * ($temperature - 100.0) * $temperature * $temperature * $temperature;
|
||||
}
|
||||
|
||||
return 1000.0 * $factor;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user