////state` and a bridge health topic under * `ring//status`. Vendor specifics stay here (H3). * * NOTE: the exact per-entity topic shape should be validated against a live ring-mqtt instance * when the user connects their account; the parser is deliberately defensive so unrecognized * topics are ignored rather than creating junk devices. */ class RingTopics { /** Topics the listener subscribes to. */ public static function subscriptions(): array { return ['ring/#']; } /** * Parse a device state topic. * * @return array{location:string,category:string,ring_id:string,entity:string}|null */ public static function parseState(string $topic): ?array { $parts = explode('/', $topic); if (($parts[0] ?? null) !== 'ring' || end($parts) !== 'state') { return null; } // ring/////state if (count($parts) === 6) { return ['location' => $parts[1], 'category' => $parts[2], 'ring_id' => $parts[3], 'entity' => $parts[4]]; } // ring////state → device-level; treat category as the entity if (count($parts) === 5) { return ['location' => $parts[1], 'category' => $parts[2], 'ring_id' => $parts[3], 'entity' => $parts[2]]; } return null; } /** * The bridge's own online/offline topic, e.g. "ring//status". * Returns true when this topic is a bridge status topic. */ public static function isBridgeStatus(string $topic): bool { $parts = explode('/', $topic); return ($parts[0] ?? null) === 'ring' && count($parts) === 3 && $parts[2] === 'status'; } }