Files
home_automatization/laravel-app/app/Http/Controllers/DeviceController.php
T
cacto dd551a0de3 Подсказка external_id из уже увиденных устройств при добавлении
Поле External ID при создании/изменении устройства теперь input+datalist:
можно вписать значение вручную (как раньше) или выбрать из подсказок —
идентификаторы устройств, от которых device-control-service уже
получал MQTT-телеметрию/ack (Redis-множество devices:known), но
которые ещё не зарегистрированы в Postgres. Уже зарегистрированные
id из списка подсказок исключаются.

DeviceShadow::knownExternalIds() — новый read-метод (SMEMBERS
devices:known), зеркалит то же множество, что Go-сервисы уже читают/
пишут для health-check/online-детекции.
2026-08-18 03:10:28 +05:00

171 lines
5.7 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Http\Requests\DeviceRequest;
use App\Http\Resources\DeviceResource;
use App\Http\Resources\DeviceTypeResource;
use App\Http\Resources\ZoneResource;
use App\Models\Device;
use App\Models\DeviceType;
use App\Models\Zone;
use App\Services\ClickHouseClient;
use App\Services\DeviceControlClient;
use App\Services\DeviceShadow;
use Illuminate\Http\Request;
use Inertia\Inertia;
class DeviceController extends Controller
{
public function index(DeviceShadow $shadow)
{
$this->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<string, mixed>
*/
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,
];
}
}