homeos/app/Services/ShellyStatusApplier.php

91 lines
3.1 KiB
PHP

<?php
namespace App\Services;
use App\Events\DeviceStateChanged;
use App\Jobs\Concerns\AppliesDeviceState;
use App\Models\Device;
use App\Support\Mqtt\ShellyNormalizer;
/**
* Applies Shelly component status to a device's entities — the single place state is written,
* whatever the transport (MQTT status topic OR the local HTTP `Shelly.GetStatus`). Normalizes,
* applies the user's input→contact role, upserts monotonically and broadcasts. Reused so the two
* transports can never drift.
*/
class ShellyStatusApplier
{
use AppliesDeviceState;
/**
* Apply a full status object (component => data), e.g. the result of Shelly.GetStatus.
*
* @param array<string,mixed> $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<string,mixed> $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.<idx>`). Keeps the raw `on` for reversibility.
*
* @param array{key:string,type:string,state:array<string,mixed>} $update
* @return array{key:string,type:string,state:array<string,mixed>}
*/
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],
];
}
}