273 lines
8.2 KiB
PHP
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;
|
|
}
|
|
}
|