$payload * @return array}> */ public static function normalize(string $component, array $payload): array { [$kind, $index] = array_pad(explode(':', $component, 2), 2, '0'); $updates = []; switch ($kind) { case 'switch': $updates[] = ['key' => "switch:{$index}", 'type' => 'switch', 'state' => ['on' => (bool) ($payload['output'] ?? false)]]; if (array_key_exists('apower', $payload)) { $updates[] = ['key' => "power:{$index}", 'type' => 'power', 'state' => ['watts' => (int) round((float) $payload['apower'])]]; } break; case 'light': $state = ['on' => (bool) ($payload['output'] ?? $payload['ison'] ?? false)]; if (array_key_exists('brightness', $payload)) { $state['brightness'] = (int) $payload['brightness']; } if (isset($payload['rgb']) && is_array($payload['rgb'])) { $state['rgb'] = array_map(fn ($v) => (int) $v, array_slice($payload['rgb'], 0, 3)); } $updates[] = ['key' => "light:{$index}", 'type' => 'light', 'state' => $state]; break; case 'cover': $updates[] = ['key' => "cover:{$index}", 'type' => 'cover', 'state' => ['position' => $payload['current_pos'] ?? null, 'state' => $payload['state'] ?? null]]; break; case 'input': // A digital (switch/button) input — a generic on/off sensor. It is NOT assumed // to be a window/door contact: on most relays input:0 is just the wall switch. // The user promotes specific inputs to contacts on the device page (invert-aware); // that role is applied in the ingest job, keeping this mapping pure. Analog/count // inputs carry no boolean `state` and are skipped. if (! is_bool($payload['state'] ?? null)) { break; } $updates[] = ['key' => "input:{$index}", 'type' => 'input', 'state' => ['on' => (bool) $payload['state']]]; break; case 'contact': case 'window': // Dedicated contact/window sensor — supports a 3-state position (closed/open/tilted). $updates[] = ['key' => "contact:{$index}", 'type' => 'contact', 'state' => self::contactState($payload)]; break; case 'temperature': $updates[] = ['key' => "temperature:{$index}", 'type' => 'temperature', 'state' => ['celsius' => $payload['tC'] ?? null]]; break; case 'humidity': $updates[] = ['key' => "humidity:{$index}", 'type' => 'humidity', 'state' => ['percent' => $payload['rh'] ?? null]]; break; case 'devicepower': if (($pct = data_get($payload, 'battery.percent')) !== null) { $updates[] = ['key' => "battery:{$index}", 'type' => 'battery', 'state' => ['percent' => (int) $pct]]; } break; default: // Housekeeping / unrecognized component (sys, wifi, cloud, mqtt, ws, ble, eth, // knx, matter, …). These are noise in a home UI — ignore them. The full local // Shelly.GetStatus returns all of them, so this keeps only real entities. break; } return $updates; } /** * Normalize a contact/window payload to {open, position}. `position` is the richer signal * (closed | open | tilted) when the sensor reports it; otherwise it is derived from a boolean. * * @param array $payload * @return array{open:bool,position:string} */ private static function contactState(array $payload): array { // Prefer an explicit 3-state position/tilt string when the sensor provides one. $position = $payload['position'] ?? $payload['tilt'] ?? null; if (is_string($position)) { $position = strtolower($position); $position = in_array($position, ['closed', 'open', 'tilted'], true) ? $position : null; } else { $position = null; } if ($position === null) { $open = (bool) ($payload['open'] ?? $payload['state'] ?? false); $position = $open ? 'open' : 'closed'; } return ['open' => $position !== 'closed', 'position' => $position]; } }