rpc->info($ip); // {id, model, gen, name, …} — throws if unreachable $status = $this->rpc->status($ip); $shellyId = $info['id'] ?? null; // Reuse an existing row for this device (already added locally, or auto-onboarded via MQTT // under the same id) so switching to local control never creates a duplicate. $device = Device::where('config->ip', $ip)->first() ?? ($shellyId ? Device::where('config->mqtt_prefix', $shellyId)->first() : null) ?? new Device; $device->fill([ 'name' => $name ?: ($device->name ?: ($info['name'] ?? $shellyId ?? $ip)), 'vendor' => 'Shelly', 'model' => $info['model'] ?? $device->model, 'protocol' => 'http', 'status' => 'active', 'last_seen_at' => now(), ]); if ($roomId !== null) { $device->room_id = $roomId; } $device->config = array_merge($device->config ?? [], array_filter([ 'ip' => $ip, 'transport' => 'local', 'mqtt_prefix' => $shellyId, // keep so discovery dedup + optional MQTT still work ], fn ($v) => $v !== null)); $device->save(); $this->applier->applyStatus($device, $status, (int) round(microtime(true) * 1_000_000)); return $device; } }