}> */ public static function normalize(string $entity, string $rawPayload): array { return match ($entity) { 'ding' => [['key' => 'ding', 'type' => 'ding', 'state' => ['on' => self::isOn($rawPayload)]]], 'motion' => [['key' => 'motion', 'type' => 'motion', 'state' => ['on' => self::isOn($rawPayload)]]], 'contact', 'contactSensor', 'contact_sensor' => [['key' => 'contact', 'type' => 'contact', 'state' => ['open' => self::isOpen($rawPayload)]]], 'lock' => [['key' => 'lock', 'type' => 'lock', 'state' => ['locked' => self::isLocked($rawPayload)]]], 'info' => self::fromInfo($rawPayload), default => [], }; } /** The info topic carries a JSON blob with battery/signal attributes. */ private static function fromInfo(string $rawPayload): array { $data = json_decode($rawPayload, true); if (! is_array($data)) { return []; } $updates = []; $battery = $data['batteryLevel'] ?? $data['battery_level'] ?? null; if ($battery !== null && is_numeric($battery)) { $updates[] = ['key' => 'battery', 'type' => 'battery', 'state' => ['percent' => (int) $battery]]; } return $updates; } private static function isOn(string $payload): bool { return in_array(strtoupper(trim($payload)), ['ON', 'TRUE', '1'], true); } private static function isOpen(string $payload): bool { return in_array(strtolower(trim($payload)), ['open', 'on', 'true', '1'], true); } private static function isLocked(string $payload): bool { return strtolower(trim($payload)) === 'locked'; } }