91 lines
3.1 KiB
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],
|
|
];
|
|
}
|
|
}
|