Files
Enelix-Utils/ShellyModul/libs/ShellyParser.php
T
dh 1baa63f23e
Tests / test (push) Successful in 46s
Shelly Modul generisch erweitern
2026-09-17 10:19:56 +00:00

273 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
final class ShellyParser
{
/**
* @return null|array{deviceTopic: string, kind: string, component: ?string}
*/
public static function parseTopic(string $topic): ?array
{
$topic = trim($topic, '/');
if ($topic === '') {
return null;
}
$patterns = [
'rpc' => '#^(.+)/events/rpc$#',
'component_status' => '#^(.+)/status/([^/]+)$#',
'status' => '#^(.+)/status$#',
'online' => '#^(.+)/online$#',
'announce' => '#^(.+)/announce$#',
];
foreach ($patterns as $kind => $pattern) {
if (preg_match($pattern, $topic, $matches) !== 1) {
continue;
}
return [
'deviceTopic' => $matches[1],
'kind' => $kind,
'component' => $matches[2] ?? null,
];
}
return null;
}
public static function extractType(string $source): string
{
if (preg_match('/^shelly([a-z0-9]+)-/i', trim($source), $matches) !== 1) {
return 'unknown';
}
return strtolower($matches[1]);
}
/**
* @return array<int, array{component: string, path: string, value: bool|int|float|string}>
*/
public static function pointsFromRpc(array $rpc): array
{
$method = $rpc['method'] ?? null;
$params = $rpc['params'] ?? null;
if (!is_string($method) || !is_array($params)) {
return [];
}
if ($method === 'NotifyStatus') {
return self::pointsFromStatus($params);
}
if ($method === 'NotifyEvent') {
return self::pointsFromEvents($params);
}
return [];
}
/**
* @return array<int, array{component: string, path: string, value: bool|int|float|string}>
*/
public static function pointsFromStatus(array $status): array
{
$points = [];
foreach ($status as $component => $value) {
if ($component === 'ts' && (is_int($value) || is_float($value))) {
$points[] = self::point('meta', 'ts', $value);
continue;
}
if (!is_string($component) || !is_array($value)) {
continue;
}
self::flatten($component, $value, '', $points);
}
return $points;
}
/**
* @return array<int, array{component: string, path: string, value: bool|int|float|string}>
*/
public static function pointsFromComponent(string $component, array $status): array
{
$points = [];
self::flatten($component, $status, '', $points);
return $points;
}
/**
* @return array<int, array{component: string, path: string, value: bool|int|float|string}>
*/
public static function pointsFromAnnounce(array $announce): array
{
$points = [];
self::flatten('device', $announce, '', $points);
return $points;
}
/**
* @return array<int, array{component: string, path: string, value: bool|int|float|string}>
*/
private static function pointsFromEvents(array $params): array
{
$points = [];
$events = $params['events'] ?? [];
if (!is_array($events)) {
return $points;
}
foreach ($events as $event) {
if (!is_array($event)) {
continue;
}
$component = $event['component'] ?? 'event';
if (!is_string($component) || $component === '') {
$component = 'event';
}
unset($event['component']);
self::flatten($component, $event, 'event', $points);
}
return $points;
}
/**
* @param array<int, array{component: string, path: string, value: bool|int|float|string}> $points
*/
private static function flatten(string $component, array $data, string $prefix, array &$points): void
{
foreach ($data as $key => $value) {
$key = (string) $key;
$path = $prefix === '' ? $key : $prefix . '.' . $key;
if ($value === null) {
continue;
}
if (is_array($value)) {
if (self::isList($value)) {
$json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($json !== false) {
$points[] = self::point($component, $path, $json);
}
} else {
self::flatten($component, $value, $path, $points);
}
continue;
}
if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) {
$points[] = self::point($component, $path, $value);
}
}
}
/**
* @param bool|int|float|string $value
* @return array{component: string, path: string, value: bool|int|float|string}
*/
private static function point(string $component, string $path, $value): array
{
return ['component' => $component, 'path' => $path, 'value' => $value];
}
private static function isList(array $value): bool
{
$expectedKey = 0;
foreach ($value as $key => $_) {
if ($key !== $expectedKey) {
return false;
}
++$expectedKey;
}
return true;
}
public static function groupForPoint(string $component, string $path): string
{
$componentType = strtolower(explode(':', $component, 2)[0]);
$path = strtolower($path);
if (in_array($componentType, ['meta', 'device'], true)) {
return 'device';
}
if ($componentType === 'input') {
return 'inputs';
}
if (preg_match('/(^|\.)(aenergy|ret_aenergy|energy|[a-z_]+_energy|total_act|total_act_ret)(\.|$)/', $path) === 1) {
return 'energy';
}
if (preg_match('/(^|\.)(apower|aprtpower|power|[a-z_]+_power)(\.|$)/', $path) === 1) {
return 'power';
}
if (preg_match('/(^|\.)(voltage|[a-z]_voltage)(\.|$)/', $path) === 1) {
return 'voltage';
}
if (preg_match('/(^|\.)(current|[a-z]_current)(\.|$)/', $path) === 1) {
return 'current';
}
if (preg_match('/(^|\.)(freq|[a-z]_freq)(\.|$)/', $path) === 1) {
return 'frequency';
}
if (preg_match('/(^|\.)(pf|[a-z]_pf)(\.|$)/', $path) === 1) {
return 'power_factor';
}
if (preg_match('/(^|\.)(temperature|tc|tf)(\.|$)/', $path) === 1 || $componentType === 'temperature') {
return 'temperature';
}
if (preg_match('/(^|\.)(humidity|rh)(\.|$)/', $path) === 1 || $componentType === 'humidity') {
return 'humidity';
}
if (preg_match('/(^|\.)(illuminance|lux)(\.|$)/', $path) === 1 || $componentType === 'illuminance') {
return 'illuminance';
}
if (preg_match('/(^|\.)(battery)(\.|$)/', $path) === 1 || $componentType === 'devicepower') {
return 'battery';
}
if ($componentType === 'switch') {
return 'switches';
}
if ($componentType === 'cover') {
return 'covers';
}
if (in_array($componentType, ['light', 'rgb', 'rgbw', 'cct', 'rgbcct'], true)) {
return 'lights';
}
if (in_array($componentType, ['pm1', 'em', 'em1', 'emdata'], true)) {
return 'meters';
}
if (in_array($componentType, ['smoke', 'flood', 'gas'], true)) {
return 'environment';
}
if (in_array($componentType, ['sys', 'wifi', 'eth', 'mqtt', 'cloud', 'ws', 'ble', 'modbus'], true)) {
return 'system';
}
return 'other';
}
/**
* @param bool|int|float|string $value
*/
public static function variableType($value, string $path): int
{
if (is_bool($value)) {
return 0;
}
if (is_string($value)) {
return 3;
}
if ((is_int($value) || is_float($value))
&& preg_match('/(^|\.)(id|ts|uptime|unixtime|[a-z_]+_ts|[a-z_]+_rev|rssi)$/i', $path) === 1) {
return 1;
}
return 2;
}
}