@@ -5,8 +5,38 @@ declare(strict_types=1);
|
||||
final class ShellyParser
|
||||
{
|
||||
/**
|
||||
* Extrahiert den Modellteil aus einer Shelly-RPC-Source.
|
||||
* @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) {
|
||||
@@ -17,120 +47,226 @@ final class ShellyParser
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{outputs: array<int, bool>, inputs: array<int, bool>, temperature: ?float}
|
||||
* @return array<int, array{component: string, path: string, value: bool|int|float|string}>
|
||||
*/
|
||||
public static function mapParams(array $params): array
|
||||
public static function pointsFromRpc(array $rpc): array
|
||||
{
|
||||
$mapped = [
|
||||
'outputs' => [],
|
||||
'inputs' => [],
|
||||
'temperature' => null,
|
||||
];
|
||||
$method = $rpc['method'] ?? null;
|
||||
$params = $rpc['params'] ?? null;
|
||||
if (!is_string($method) || !is_array($params)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($params as $component => $value) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (preg_match('/^switch:(\d+)$/', $component, $matches) === 1) {
|
||||
$index = (int) $matches[1];
|
||||
$output = self::toBoolean($value['output'] ?? null);
|
||||
$input = self::toBoolean($value['input'] ?? null);
|
||||
|
||||
if ($output !== null) {
|
||||
$mapped['outputs'][$index] = $output;
|
||||
}
|
||||
if ($input !== null) {
|
||||
$mapped['inputs'][$index] = $input;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/^input:(\d+)$/', $component, $matches) === 1) {
|
||||
$state = self::toBoolean($value['state'] ?? null);
|
||||
if ($state !== null) {
|
||||
$mapped['inputs'][(int) $matches[1]] = $state;
|
||||
}
|
||||
}
|
||||
self::flatten($component, $value, '', $points);
|
||||
}
|
||||
|
||||
$mapped['temperature'] = self::findTemperature($params);
|
||||
ksort($mapped['outputs']);
|
||||
ksort($mapped['inputs']);
|
||||
|
||||
return $mapped;
|
||||
return $points;
|
||||
}
|
||||
|
||||
public static function toBoolean($value): ?bool
|
||||
/**
|
||||
* @return array<int, array{component: string, path: string, value: bool|int|float|string}>
|
||||
*/
|
||||
public static function pointsFromComponent(string $component, array $status): array
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
$points = [];
|
||||
self::flatten($component, $status, '', $points);
|
||||
|
||||
if ($value === 1 || $value === '1') {
|
||||
return true;
|
||||
}
|
||||
if ($value === 0 || $value === '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$normalized = strtolower(trim($value));
|
||||
if (in_array($normalized, ['true', 'on', 'online'], true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($normalized, ['false', 'off', 'offline'], true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return $points;
|
||||
}
|
||||
|
||||
private static function findTemperature(array $data): ?float
|
||||
/**
|
||||
* @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) {
|
||||
if (!is_string($key)) {
|
||||
$key = (string) $key;
|
||||
$path = $prefix === '' ? $key : $prefix . '.' . $key;
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lowerKey = strtolower($key);
|
||||
if ($lowerKey === 'temperature') {
|
||||
$temperature = self::temperatureFromValue($value);
|
||||
if ($temperature !== null) {
|
||||
return $temperature;
|
||||
}
|
||||
}
|
||||
|
||||
if ($lowerKey === 'tc' && is_numeric($value)) {
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$temperature = self::findTemperature($value);
|
||||
if ($temperature !== null) {
|
||||
return $temperature;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function temperatureFromValue($value): ?float
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
return (float) $value;
|
||||
}
|
||||
if (!is_array($value)) {
|
||||
return null;
|
||||
}
|
||||
return ['component' => $component, 'path' => $path, 'value' => $value];
|
||||
}
|
||||
|
||||
foreach (['tC', 'tc', 't'] as $key) {
|
||||
if (array_key_exists($key, $value) && is_numeric($value[$key])) {
|
||||
return (float) $value[$key];
|
||||
private static function isList(array $value): bool
|
||||
{
|
||||
$expectedKey = 0;
|
||||
foreach ($value as $key => $_) {
|
||||
if ($key !== $expectedKey) {
|
||||
return false;
|
||||
}
|
||||
++$expectedKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user