data), e.g. the result of Shelly.GetStatus. * * @param array $status */ public function applyStatus(Device $device, array $status, int $observedAt): void { foreach ($status as $component => $data) { if (is_array($data)) { $this->applyComponent($device, $component, $data, $observedAt); } } } /** * Apply one component's payload (e.g. "switch:0" => {...}). * * @param array $data */ public function applyComponent(Device $device, string $component, array $data, int $observedAt): void { foreach (ShellyNormalizer::normalize($component, $data) as $update) { $update = $this->applyInputRole($device, $update); $entity = $device->entities()->firstOrCreate( ['key' => $update['key']], ['type' => $update['type']], ); // Keep the entity type in sync with its (config-driven) role — e.g. an input promoted // to a contact, or demoted back. if ($entity->type !== $update['type']) { $entity->forceFill(['type' => $update['type']])->save(); } if ($this->applyState($entity->id, $update['state'], $observedAt)) { DeviceStateChanged::dispatch($device->uuid, $entity->key, $update['state']); } } } /** * Promote a raw `input` update to a `contact` when the user assigned that input a window/door * role on the device page (config `input_roles.`). Keeps the raw `on` for reversibility. * * @param array{key:string,type:string,state:array} $update * @return array{key:string,type:string,state:array} */ public function applyInputRole(Device $device, array $update): array { if ($update['type'] !== 'input') { return $update; } $index = explode(':', $update['key'])[1] ?? '0'; $role = data_get($device->config, "input_roles.{$index}"); if (! is_array($role)) { return $update; } $on = (bool) ($update['state']['on'] ?? false); $open = ($role['invert'] ?? false) ? ! $on : $on; return [ 'key' => $update['key'], 'type' => 'contact', 'state' => ['open' => $open, 'position' => $open ? 'open' : 'closed', 'kind' => $role['kind'] ?? 'window', 'on' => $on], ]; } }