RegisterPropertyString( 'Kanalauswahl', json_encode(CC100ChannelModel::defaultSelection(), JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) ); $this->RegisterPropertyString( 'DigitalInputPath', '/sys/devices/platform/soc/44009000.spi/spi_master/spi0/spi0.0/din' ); $this->RegisterPropertyString('DigitalOutputPath', '/sys/kernel/dout_drv/DOUT_DATA'); $this->RegisterPropertyString('CalibrationPath', '/etc/calib'); $this->RegisterPropertyString('AI1Path', '/sys/bus/iio/devices/iio:device3/in_voltage3_raw'); $this->RegisterPropertyString('AI2Path', '/sys/bus/iio/devices/iio:device3/in_voltage0_raw'); $this->RegisterPropertyString('AO1ValuePath', '/sys/bus/iio/devices/iio:device0/out_voltage1_raw'); $this->RegisterPropertyString('AO1PowerPath', '/sys/bus/iio/devices/iio:device0/out_voltage1_powerdown'); $this->RegisterPropertyString('AO2ValuePath', '/sys/bus/iio/devices/iio:device1/out_voltage2_raw'); $this->RegisterPropertyString('AO2PowerPath', '/sys/bus/iio/devices/iio:device1/out_voltage2_powerdown'); $this->RegisterPropertyString('PT1Path', '/sys/bus/iio/devices/iio:device2/in_voltage13_raw'); $this->RegisterPropertyString('PT2Path', '/sys/bus/iio/devices/iio:device2/in_voltage1_raw'); $this->RegisterPropertyBoolean('Debug', false); $this->RegisterAttributeString('LastPoll', '{}'); $this->RegisterAttributeString('Calibration', '{}'); $this->RegisterTimer( 'PollChannels', 0, 'IPS_RequestAction(' . $this->InstanceID . ', "Poll", true);' ); $this->RegisterVariableInteger('Hardwarestatus', 'Hardwarestatus', '', 900); $this->RegisterVariableString('Stoertext', 'Stoertext', '', 901); } public function ApplyChanges() { parent::ApplyChanges(); try { $channels = CC100ChannelModel::normalizeSelection($this->ReadPropertyString('Kanalauswahl')); $this->synchronizeVariables($channels); $activeChannels = array_values(array_filter( $channels, static fn (array $channel): bool => $channel['active'] )); $this->SetSummary(count($activeChannels) . ' Kanaele aktiv'); $this->WriteAttributeString('LastPoll', '{}'); $this->WriteAttributeString('Calibration', '{}'); if ($activeChannels === []) { $this->SetTimerInterval('PollChannels', 0); $this->setHardwareStatus(0, 'Keine Kanaele aktiviert.'); $this->SetStatus(IS_ACTIVE); return; } $intervals = array_column($activeChannels, 'pollInterval'); $this->SetTimerInterval('PollChannels', min($intervals)); $this->PollChannels(true); } catch (Throwable $exception) { $this->SetTimerInterval('PollChannels', 0); $this->setHardwareStatus(3, $exception->getMessage()); $this->SetStatus(self::STATUS_CONFIGURATION_INVALID); $this->debug('Konfiguration', $exception->getMessage()); } } public function RequestAction($Ident, $Value) { if ($Ident === 'Poll') { $this->PollChannels((bool) $Value); return; } if (preg_match('/^(DO|AO)([1-9][0-9]*)$/', (string) $Ident, $matches) !== 1) { throw new InvalidArgumentException('Unbekannte Aktion ' . $Ident . '.'); } $type = $matches[1]; $channel = (int) $matches[2]; if ($type === 'DO') { $this->SetDigitalOutput($channel, (bool) $Value); return; } if (!is_int($Value) && !is_float($Value)) { throw new InvalidArgumentException('Analoger Ausgang erwartet einen Zahlenwert in Volt.'); } $this->SetAnalogOutput($channel, (float) $Value); } public function PollChannels(bool $force = false): void { try { $channels = array_values(array_filter( CC100ChannelModel::normalizeSelection($this->ReadPropertyString('Kanalauswahl')), static fn (array $channel): bool => $channel['active'] )); } catch (Throwable $exception) { $this->setHardwareStatus(3, $exception->getMessage()); $this->SetStatus(self::STATUS_CONFIGURATION_INVALID); return; } if ($channels === []) { return; } $now = (int) round(microtime(true) * 1000); $lastPoll = json_decode($this->ReadAttributeString('LastPoll'), true); if (!is_array($lastPoll)) { $lastPoll = []; } $due = array_values(array_filter( $channels, static fn (array $channel): bool => $force || $now - (int) ($lastPoll[$channel['key']] ?? 0) >= $channel['pollInterval'] )); if ($due === []) { return; } $errors = []; $fileCache = []; foreach ($due as $channel) { try { $value = $this->readChannel($channel, $fileCache); $this->setValueIfChanged($channel['key'], $value); } catch (Throwable $exception) { $errors[] = $channel['key'] . ': ' . $exception->getMessage(); $this->debug('Lesefehler ' . $channel['key'], $exception->getMessage()); } $lastPoll[$channel['key']] = $now; } $this->WriteAttributeString( 'LastPoll', json_encode($lastPoll, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) ); if ($errors === []) { $this->setHardwareStatus(1, ''); $this->SetStatus(IS_ACTIVE); return; } $this->setHardwareStatus(2, implode(' | ', $errors)); $this->SetStatus(self::STATUS_HARDWARE_UNAVAILABLE); } public function SetDigitalOutput(int $channel, bool $state): void { $key = 'DO' . $channel; $this->assertWritableSelected($key); try { $path = $this->ReadPropertyString('DigitalOutputPath'); $aggregate = CC100ChannelModel::parseInteger($this->readFile($path)); $newValue = CC100ChannelModel::withBitState($aggregate, $channel, $state); if ($newValue !== $aggregate) { $this->writeFile($path, (string) $newValue); } $this->setValueIfChanged($key, $state); $this->PollChannels(true); } catch (Throwable $exception) { $this->setHardwareStatus(3, $key . ': ' . $exception->getMessage()); $this->SetStatus(self::STATUS_WRITE_FAILED); throw $exception; } } public function SetAnalogOutput(int $channel, float $voltage): void { $key = 'AO' . $channel; $this->assertWritableSelected($key); if (!is_finite($voltage) || $voltage < 0.0 || $voltage > 10.0) { throw new InvalidArgumentException('Analoger Ausgang muss zwischen 0 und 10 V liegen.'); } try { $calibration = $this->calibrationFor($key); $raw = CC100ChannelModel::engineeringToRaw($voltage * 1000.0, $calibration); $this->writeFile($this->ReadPropertyString($key . 'PowerPath'), '0'); $this->writeFile($this->ReadPropertyString($key . 'ValuePath'), (string) $raw); $this->setValueIfChanged($key, round($voltage, 3)); $this->PollChannels(true); } catch (Throwable $exception) { $this->setHardwareStatus(3, $key . ': ' . $exception->getMessage()); $this->SetStatus(self::STATUS_WRITE_FAILED); throw $exception; } } /** * @param list $channels */ private function synchronizeVariables(array $channels): void { $configured = []; foreach ($channels as $channel) { $configured[$channel['key']] = $channel; if (!$channel['active']) { continue; } $definition = CC100ChannelModel::definition($channel['type'], $channel['channel']); if ($definition['variableType'] === 0) { $this->RegisterVariableBoolean( $channel['key'], $channel['name'], $definition['profile'], $definition['position'] ); } else { $this->RegisterVariableFloat( $channel['key'], $channel['name'], $definition['profile'], $definition['position'] ); } if ($definition['writable']) { $this->EnableAction($channel['key']); } } foreach (CC100ChannelModel::defaultSelection() as $default) { $key = $default['Typ'] . $default['Kanal']; if (($configured[$key]['active'] ?? false) === true) { continue; } if (@$this->GetIDForIdent($key) !== false) { $this->UnregisterVariable($key); } } } /** * @param array{key: string, type: string, channel: int} $channel * @param array $fileCache * @return bool|float */ private function readChannel(array $channel, array &$fileCache) { $type = $channel['type']; $number = $channel['channel']; if ($type === 'DI' || $type === 'DO') { $property = $type === 'DI' ? 'DigitalInputPath' : 'DigitalOutputPath'; $path = $this->ReadPropertyString($property); if (!array_key_exists($path, $fileCache)) { $fileCache[$path] = CC100ChannelModel::parseInteger($this->readFile($path)); } return CC100ChannelModel::bitState($fileCache[$path], $number); } $key = $type . $number; $raw = CC100ChannelModel::parseInteger($this->readFile($this->pathFor($key))); $engineering = CC100ChannelModel::rawToEngineering($raw, $this->calibrationFor($key)); if ($type === 'PT') { return CC100ChannelModel::pt1000Temperature($engineering); } return round($engineering / 1000.0, 3); } private function pathFor(string $key): string { if (str_starts_with($key, 'AI') || str_starts_with($key, 'PT')) { return $this->ReadPropertyString($key . 'Path'); } if (str_starts_with($key, 'AO')) { return $this->ReadPropertyString($key . 'ValuePath'); } throw new InvalidArgumentException('Kein Einzelpfad fuer ' . $key . '.'); } /** @return array{x1: float, y1: float, x2: float, y2: float} */ private function calibrationFor(string $key): array { $stored = json_decode($this->ReadAttributeString('Calibration'), true); if (!is_array($stored) || $stored === []) { $stored = CC100ChannelModel::parseCalibration( $this->readFile($this->ReadPropertyString('CalibrationPath')) ); $this->WriteAttributeString( 'Calibration', json_encode($stored, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) ); } if (!isset($stored[$key]) || !is_array($stored[$key])) { throw new RuntimeException('Kalibrierung fuer ' . $key . ' fehlt.'); } return $stored[$key]; } private function assertWritableSelected(string $key): void { $channels = CC100ChannelModel::normalizeSelection($this->ReadPropertyString('Kanalauswahl')); foreach ($channels as $channel) { if ($channel['key'] !== $key || !$channel['active']) { continue; } $definition = CC100ChannelModel::definition($channel['type'], $channel['channel']); if ($definition['writable']) { return; } } throw new RuntimeException($key . ' ist nicht als bedienbarer Kanal aktiviert.'); } private function readFile(string $path): string { if ($path === '' || !is_readable($path)) { throw new RuntimeException('Datei ist nicht lesbar: ' . $path); } $content = @file_get_contents($path); if ($content === false) { throw new RuntimeException('Datei konnte nicht gelesen werden: ' . $path); } return $content; } private function writeFile(string $path, string $value): void { if ($path === '') { throw new RuntimeException('Ausgabepfad ist leer.'); } $written = @file_put_contents($path, $value); if ($written === false) { throw new RuntimeException('Datei konnte nicht geschrieben werden: ' . $path); } $this->debug('Hardware schreiben', $path . ' <- ' . $value); } /** @param bool|float $value */ private function setValueIfChanged(string $ident, $value): void { $variableID = @$this->GetIDForIdent($ident); if ($variableID === false) { return; } $current = $this->GetValue($ident); if (is_float($value)) { if (is_float($current) && abs($current - $value) < 0.0001) { return; } } elseif ($current === $value) { return; } $this->SetValue($ident, $value); } private function setHardwareStatus(int $status, string $message): void { $this->setValueIfChanged('Hardwarestatus', $status); $this->setValueIfChanged('Stoertext', $message); } /** @param mixed $message */ private function debug(string $title, $message): void { if (!$this->ReadPropertyBoolean('Debug')) { return; } if (!is_string($message)) { $message = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } $this->SendDebug($title, (string) $message, 0); } }