feat(utils): adapt WAGO CC100 hardware module
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixUtils\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CC100HardwareModulstrukturTest extends TestCase
|
||||
{
|
||||
public function testModuleMetadataAndFormAreComplete(): void
|
||||
{
|
||||
$module = json_decode(
|
||||
file_get_contents(__DIR__ . '/../CC100Hardware/module.json'),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
$form = json_decode(
|
||||
file_get_contents(__DIR__ . '/../CC100Hardware/form.json'),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
|
||||
self::assertSame('{D62EA550-68A5-48E2-BF31-1C721AA1BD4E}', $module['id']);
|
||||
self::assertSame('CC100 Hardware', $module['name']);
|
||||
self::assertSame('CC100', $module['prefix']);
|
||||
self::assertNotEmpty($form['elements']);
|
||||
self::assertNotEmpty($form['status']);
|
||||
}
|
||||
|
||||
public function testModuleProvidesEveryCc100IoGroupAndConfigurablePolling(): void
|
||||
{
|
||||
$module = file_get_contents(__DIR__ . '/../CC100Hardware/module.php');
|
||||
$form = file_get_contents(__DIR__ . '/../CC100Hardware/form.json');
|
||||
|
||||
self::assertNotFalse($module);
|
||||
self::assertNotFalse($form);
|
||||
foreach ([
|
||||
'DigitalInputPath',
|
||||
'DigitalOutputPath',
|
||||
'AI1Path',
|
||||
'AI2Path',
|
||||
'AO1ValuePath',
|
||||
'AO2ValuePath',
|
||||
'PT1Path',
|
||||
'PT2Path',
|
||||
] as $property) {
|
||||
self::assertStringContainsString($property, $module);
|
||||
self::assertStringContainsString($property, $form);
|
||||
}
|
||||
self::assertStringContainsString('Abfrageintervall', $form);
|
||||
self::assertStringContainsString("RegisterTimer(\n 'PollChannels'", $module);
|
||||
}
|
||||
|
||||
public function testOutputsAreOnlyWrittenByExplicitActions(): void
|
||||
{
|
||||
$module = file_get_contents(__DIR__ . '/../CC100Hardware/module.php');
|
||||
self::assertNotFalse($module);
|
||||
|
||||
$applyStart = strpos($module, 'public function ApplyChanges');
|
||||
$actionStart = strpos($module, 'public function RequestAction');
|
||||
self::assertNotFalse($applyStart);
|
||||
self::assertNotFalse($actionStart);
|
||||
self::assertStringNotContainsString(
|
||||
'writeFile(',
|
||||
substr($module, $applyStart, $actionStart - $applyStart)
|
||||
);
|
||||
self::assertStringNotContainsString('TryWriteFile', $module);
|
||||
self::assertStringNotContainsString('usleep(', $module);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixUtils\Tests;
|
||||
|
||||
use Belevo\EnelixUtils\CC100\CC100ChannelModel;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once __DIR__ . '/../CC100Hardware/libs/CC100ChannelModel.php';
|
||||
|
||||
final class CC100HardwareTest extends TestCase
|
||||
{
|
||||
public function testDefaultSelectionContainsEveryOnboardChannelExactlyOnce(): void
|
||||
{
|
||||
$selection = CC100ChannelModel::defaultSelection();
|
||||
|
||||
self::assertCount(18, $selection);
|
||||
self::assertCount(
|
||||
18,
|
||||
array_unique(array_map(
|
||||
static fn (array $row): string => $row['Typ'] . $row['Kanal'],
|
||||
$selection
|
||||
))
|
||||
);
|
||||
self::assertSame([], array_filter(
|
||||
$selection,
|
||||
static fn (array $row): bool => $row['Aktiv']
|
||||
));
|
||||
}
|
||||
|
||||
public function testSelectionNormalizesNamesAndIntervals(): void
|
||||
{
|
||||
$selection = CC100ChannelModel::normalizeSelection(json_encode([
|
||||
[
|
||||
'Aktiv' => true,
|
||||
'Typ' => 'di',
|
||||
'Kanal' => 1,
|
||||
'Anzeigename' => '',
|
||||
'Abfrageintervall' => 250,
|
||||
],
|
||||
[
|
||||
'Aktiv' => true,
|
||||
'Typ' => 'AO',
|
||||
'Kanal' => 2,
|
||||
'Anzeigename' => 'Ventil',
|
||||
'Abfrageintervall' => 5000,
|
||||
],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
|
||||
self::assertSame('DI1', $selection[0]['key']);
|
||||
self::assertSame('Digitaler Eingang 1', $selection[0]['name']);
|
||||
self::assertSame(250, $selection[0]['pollInterval']);
|
||||
self::assertSame('AO2', $selection[1]['key']);
|
||||
self::assertSame('Ventil', $selection[1]['name']);
|
||||
}
|
||||
|
||||
public function testDuplicateChannelIsRejected(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
CC100ChannelModel::normalizeSelection(json_encode([
|
||||
['Typ' => 'DI', 'Kanal' => 1, 'Abfrageintervall' => 500],
|
||||
['Typ' => 'DI', 'Kanal' => 1, 'Abfrageintervall' => 500],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
public function testCalibrationSupportsWagoAZeroOneHeaderAndBothDirections(): void
|
||||
{
|
||||
$calibration = CC100ChannelModel::parseCalibration(
|
||||
"AI1 A01\n100 0 1100 10000\n200 0 1200 10000\n"
|
||||
);
|
||||
|
||||
self::assertArrayHasKey('AO1', $calibration);
|
||||
self::assertSame(5000.0, CC100ChannelModel::rawToEngineering(600, $calibration['AI1']));
|
||||
self::assertSame(700, CC100ChannelModel::engineeringToRaw(5000, $calibration['AO1']));
|
||||
}
|
||||
|
||||
public function testPt1000CurveAndBitOperationsAreStable(): void
|
||||
{
|
||||
self::assertSame(0.0, CC100ChannelModel::pt1000Temperature(1000.0));
|
||||
self::assertEqualsWithDelta(100.0, CC100ChannelModel::pt1000Temperature(1385.05), 0.1);
|
||||
|
||||
self::assertTrue(CC100ChannelModel::bitState(5, 1));
|
||||
self::assertFalse(CC100ChannelModel::bitState(5, 2));
|
||||
self::assertTrue(CC100ChannelModel::bitState(5, 3));
|
||||
self::assertSame(7, CC100ChannelModel::withBitState(5, 2, true));
|
||||
self::assertSame(4, CC100ChannelModel::withBitState(5, 1, false));
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ final class DokumentationsstrukturTest extends TestCase
|
||||
return [
|
||||
'Verbrauchskostenreport' => ['Verbrauchskostenreport', 'Status: Implementiert'],
|
||||
'Virtuelle Batterie' => ['Virtuelle-Batterie', 'Status: Diskussionsentwurf'],
|
||||
'CC100 Hardware' => ['CC100-Hardware', 'Status: Diskussionsentwurf'],
|
||||
'CC100 Hardware' => ['CC100-Hardware', 'Status: Implementiert'],
|
||||
'Energiediagramm' => ['Energiediagramm', 'Status: Implementiert'],
|
||||
'VGT-Schnittstelle' => ['VGT-Schnittstelle', 'Status: Diskussionsentwurf'],
|
||||
'Shelly-Modul' => ['Shelly-Modul', 'Status: Implementiert'],
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'CC100Hardware' => [
|
||||
'file' => __DIR__ . '/modules/CC100Hardware.php',
|
||||
'paths' => [
|
||||
'CC100Hardware/*',
|
||||
],
|
||||
],
|
||||
'Energiediagramm' => [
|
||||
'file' => __DIR__ . '/modules/Energiediagramm.php',
|
||||
'paths' => [
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Belevo\EnelixUtils\SymconTest\TestContext;
|
||||
|
||||
return static function (TestContext $test): void {
|
||||
$temporaryFiles = [];
|
||||
$createFile = static function (string $content) use (&$temporaryFiles): string {
|
||||
$path = tempnam(sys_get_temp_dir(), 'enelix_cc100_');
|
||||
if ($path === false || file_put_contents($path, $content) === false) {
|
||||
throw new RuntimeException('Temporäre CC100-Testdatei konnte nicht erstellt werden.');
|
||||
}
|
||||
$temporaryFiles[] = $path;
|
||||
return $path;
|
||||
};
|
||||
|
||||
$digitalInput = $createFile('5');
|
||||
$digitalOutput = $createFile('0');
|
||||
$calibration = $createFile(
|
||||
"PT1 PT2 AI1 AI2 A01 A02\n"
|
||||
. "1000 1000 2000 1385.05\n"
|
||||
. "1000 1000 2000 1385.05\n"
|
||||
. "100 0 1100 10000\n"
|
||||
. "100 0 1100 10000\n"
|
||||
. "200 0 1200 10000\n"
|
||||
. "200 0 1200 10000\n"
|
||||
);
|
||||
$ai1 = $createFile('600');
|
||||
$ao1 = $createFile('700');
|
||||
$ao1Power = $createFile('1');
|
||||
$pt1 = $createFile('1000');
|
||||
|
||||
$channels = json_encode([
|
||||
[
|
||||
'Aktiv' => true,
|
||||
'Typ' => 'DI',
|
||||
'Kanal' => 1,
|
||||
'Anzeigename' => 'Test Eingang',
|
||||
'Abfrageintervall' => 1000,
|
||||
],
|
||||
[
|
||||
'Aktiv' => true,
|
||||
'Typ' => 'DO',
|
||||
'Kanal' => 1,
|
||||
'Anzeigename' => 'Test Ausgang',
|
||||
'Abfrageintervall' => 1000,
|
||||
],
|
||||
[
|
||||
'Aktiv' => true,
|
||||
'Typ' => 'AI',
|
||||
'Kanal' => 1,
|
||||
'Anzeigename' => 'Test Spannung Eingang',
|
||||
'Abfrageintervall' => 1000,
|
||||
],
|
||||
[
|
||||
'Aktiv' => true,
|
||||
'Typ' => 'AO',
|
||||
'Kanal' => 1,
|
||||
'Anzeigename' => 'Test Spannung Ausgang',
|
||||
'Abfrageintervall' => 1000,
|
||||
],
|
||||
[
|
||||
'Aktiv' => true,
|
||||
'Typ' => 'PT',
|
||||
'Kanal' => 1,
|
||||
'Anzeigename' => 'Test Temperatur',
|
||||
'Abfrageintervall' => 1000,
|
||||
],
|
||||
], JSON_THROW_ON_ERROR);
|
||||
|
||||
$moduleId = $test->createInstance(
|
||||
'{D62EA550-68A5-48E2-BF31-1C721AA1BD4E}',
|
||||
'CC100 Hardware',
|
||||
[
|
||||
'Kanalauswahl' => $channels,
|
||||
'DigitalInputPath' => $digitalInput,
|
||||
'DigitalOutputPath' => $digitalOutput,
|
||||
'CalibrationPath' => $calibration,
|
||||
'AI1Path' => $ai1,
|
||||
'AO1ValuePath' => $ao1,
|
||||
'AO1PowerPath' => $ao1Power,
|
||||
'PT1Path' => $pt1,
|
||||
]
|
||||
);
|
||||
|
||||
$test->runCase('Nur ausgewaehlte Kanaele werden angelegt und gelesen', static function (
|
||||
TestContext $test
|
||||
) use ($moduleId): void {
|
||||
$test->assertInstanceStatus($moduleId);
|
||||
$test->assertSame(true, GetValue($test->objectByIdent('DI1', $moduleId)));
|
||||
$test->assertSame(false, @IPS_GetObjectIDByIdent('DI2', $moduleId));
|
||||
$test->assertEquals(5.0, (float) GetValue($test->objectByIdent('AI1', $moduleId)), 0.001);
|
||||
$test->assertEquals(0.0, (float) GetValue($test->objectByIdent('PT1', $moduleId)), 0.1);
|
||||
});
|
||||
|
||||
$test->runCase('Digitaler Ausgang erhaelt die anderen Bits', static function (
|
||||
TestContext $test
|
||||
) use ($moduleId, $digitalOutput): void {
|
||||
file_put_contents($digitalOutput, '4');
|
||||
IPS_RequestAction($moduleId, 'DO1', true);
|
||||
$test->assertSame('5', trim((string) file_get_contents($digitalOutput)));
|
||||
$test->assertSame(true, GetValue($test->objectByIdent('DO1', $moduleId)));
|
||||
});
|
||||
|
||||
$test->runCase('Analoger Ausgang wird invers kalibriert geschrieben', static function (
|
||||
TestContext $test
|
||||
) use ($moduleId, $ao1, $ao1Power): void {
|
||||
IPS_RequestAction($moduleId, 'AO1', 2.5);
|
||||
$test->assertSame('0', trim((string) file_get_contents($ao1Power)));
|
||||
$test->assertSame('450', trim((string) file_get_contents($ao1)));
|
||||
$test->assertEquals(2.5, (float) GetValue($test->objectByIdent('AO1', $moduleId)), 0.001);
|
||||
});
|
||||
|
||||
foreach ($temporaryFiles as $path) {
|
||||
@unlink($path);
|
||||
}
|
||||
};
|
||||
@@ -58,7 +58,7 @@ final class SymconTestContractTest extends TestCase
|
||||
public function testAffectedModuleResolverSelectsAllModulesForFrameworkChanges(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['Energiediagramm', 'ShellyModul', 'Verbrauchskostenreport'],
|
||||
['CC100Hardware', 'Energiediagramm', 'ShellyModul', 'Verbrauchskostenreport'],
|
||||
$this->resolveAffectedModules(['tests/Symcon/TestContext.php'])
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user