state?->state, 'on', false); return $this->setOn($entity, ! $current, $source); } public function setOn(Entity $entity, bool $on, string $source = 'user'): Command { $command = $this->run( $entity->device, $entity, $on ? 'turn_on' : 'turn_off', ['on' => $on], $source, fn (DeviceDriver $driver) => $on ? $driver->turnOn($entity) : $driver->turnOff($entity), ); $this->simulateDemoEcho($command, $entity, ['on' => $on]); return $command; } public function reboot(Device $device, string $source = 'user'): Command { return $this->run($device, null, 'reboot', [], $source, fn (DeviceDriver $driver) => $driver->reboot($device)); } public function setLight(Entity $entity, array $params, string $source = 'user'): Command { $command = $this->run($entity->device, $entity, 'set_light', $params, $source, fn (DeviceDriver $driver) => $driver->setLight($entity, $params)); $this->simulateDemoEcho($command, $entity, $params); return $command; } /** * Demo devices have no real hardware to echo their new state back over MQTT, so the UI would * look dead when you toggle them. For a demo device we synthesize that echo — write the new * state and broadcast DeviceStateChanged — so the mock home is fully interactive (handoff * §13.2, "mock first"). Real devices never take this path; their actual status message does. */ private function simulateDemoEcho(Command $command, Entity $entity, array $state): void { if ($command->result !== 'ok' || ! $entity->device->demo) { return; } $merged = array_merge($entity->state?->state ?? [], $state); DeviceState::updateOrCreate( ['entity_id' => $entity->id], ['state' => $merged, 'observed_at' => (int) round(microtime(true) * 1_000_000)], ); DeviceStateChanged::dispatch($entity->device->uuid, $entity->key, $merged); } private function run(Device $device, ?Entity $entity, string $command, array $payload, string $source, callable $action): Command { $result = 'ok'; try { $action($this->driverFor($device)); // Local (HTTP) devices don't push state — re-poll so the change reflects promptly. if ($result === 'ok' && $device->protocol === 'http') { PollShellyDevice::dispatch($device->id); } } catch (\Throwable $e) { report($e); $result = 'error: '.$e->getMessage(); } return Command::create([ 'device_id' => $device->id, 'entity_id' => $entity?->id, 'source' => $source, 'user_id' => Auth::id(), 'command' => $command, 'payload' => $payload, 'result' => $result, ]); } private function driverFor(Device $device): DeviceDriver { return match (true) { $device->vendor === 'Shelly' && $device->protocol === 'http' => app(ShellyHttpDriver::class), $device->vendor === 'Shelly' && $device->protocol === 'mqtt' => app(ShellyMqttDriver::class), default => throw new \RuntimeException("No driver for {$device->vendor}/{$device->protocol}."), }; } }