'#^(.+)/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 */ 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 */ 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 */ public static function pointsFromComponent(string $component, array $status): array { $points = []; self::flatten($component, $status, '', $points); return $points; } /** * @return array */ public static function pointsFromAnnounce(array $announce): array { $points = []; self::flatten('device', $announce, '', $points); return $points; } /** * @return array */ 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 $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; } }