authorize('viewAny', Device::class); $devices = Device::with(['zone', 'deviceType'])->orderBy('name')->get(); $statuses = $shadow->statuses($devices->pluck('external_id')->all()); $devices->each(fn (Device $d) => $d->live_status = $statuses[$d->external_id] ?? 'unknown'); return Inertia::render('Devices/Index', [ 'devices' => DeviceResource::collection($devices)->resolve(), ]); } public function show(Device $device, DeviceShadow $shadow, ClickHouseClient $clickHouse) { $this->authorize('view', $device); $device->load(['zone', 'deviceType']); $snapshot = $shadow->snapshot($device->external_id); $device->live_status = $snapshot['status']; $telemetry = []; if ($device->deviceType->category->value === 'sensor') { $telemetry = $clickHouse->query( 'SELECT sensor_type, value, recorded_at FROM telemetry WHERE device_id = {device_id:String} ORDER BY recorded_at DESC LIMIT 50', ['device_id' => $device->external_id], ); } return Inertia::render('Devices/Show', [ 'device' => (new DeviceResource($device))->resolve(), 'shadow' => [ 'last_seen' => $snapshot['last_seen'], 'desired_state' => $snapshot['desired_state'], 'reported_state' => $snapshot['reported_state'], ], 'telemetry' => $telemetry, ]); } public function create(DeviceShadow $shadow) { $this->authorize('create', Device::class); return Inertia::render('Devices/Create', $this->formOptions($shadow)); } public function store(DeviceRequest $request) { $device = new Device($request->validated()); $device->user()->associate($request->user()); $device->save(); return redirect()->route('devices.index')->with('status', 'Устройство добавлено.'); } public function edit(Device $device, DeviceShadow $shadow) { $this->authorize('update', $device); return Inertia::render('Devices/Edit', [ 'device' => $device->only(['id', 'name', 'zone_id', 'device_type_id', 'external_id', 'protocol']), ...$this->formOptions($shadow), ]); } public function update(DeviceRequest $request, Device $device) { $device->update($request->validated()); return redirect()->route('devices.index')->with('status', 'Устройство обновлено.'); } public function destroy(Device $device) { $this->authorize('delete', $device); $device->delete(); return redirect()->route('devices.index')->with('status', 'Устройство удалено.'); } public function turnOn(Device $device, DeviceControlClient $client) { $this->authorize('update', $device); $this->ensureCapability($device, 'turn_on'); return $this->respondToCommand($client->turnOn($device->external_id)); } public function turnOff(Device $device, DeviceControlClient $client) { $this->authorize('update', $device); $this->ensureCapability($device, 'turn_off'); return $this->respondToCommand($client->turnOff($device->external_id)); } public function setLevel(Request $request, Device $device, DeviceControlClient $client) { $this->authorize('update', $device); $this->ensureCapability($device, 'set_level'); $validated = $request->validate(['level' => ['required', 'numeric']]); return $this->respondToCommand($client->setLevel($device->external_id, (float) $validated['level'])); } private function ensureCapability(Device $device, string $capability): void { abort_unless( in_array($capability, $device->deviceType->capabilities ?? [], true), 422, "Устройство не поддерживает действие «{$capability}».", ); } /** * @param array{success: bool, error: ?string} $result */ private function respondToCommand(array $result) { return back()->with( $result['success'] ? 'status' : 'error', $result['success'] ? 'Команда отправлена.' : ($result['error'] ?? 'Команда не выполнена.'), ); } /** * @return array */ private function formOptions(DeviceShadow $shadow): array { $registeredIds = Device::pluck('external_id')->all(); $unregisteredExternalIds = collect($shadow->knownExternalIds()) ->diff($registeredIds) ->sort() ->values(); return [ 'zones' => ZoneResource::collection(Zone::orderBy('name')->get())->resolve(), 'deviceTypes' => DeviceTypeResource::collection(DeviceType::orderBy('code')->get())->resolve(), // external_id of devices device-control-service has already seen // over MQTT but nobody has registered yet — offered as // suggestions (not a closed list) when adding/editing a device. 'unregisteredExternalIds' => $unregisteredExternalIds, ]; } }