rpc->info($ip); // {id, model, gen, name, …} — throws if unreachable $status = $this->rpc->status($ip); $shellyId = $info['id'] ?? null; // Reuse an existing row so switching to local control never duplicates. Match on the STABLE // Shelly id first; only reuse an IP match when that row is the SAME (or an unidentified) // device — otherwise a DHCP-reassigned IP would let this Shelly overwrite an unrelated one. $device = $shellyId ? Device::where('config->mqtt_prefix', $shellyId)->first() : null; if ($device === null) { $byIp = Device::where('config->ip', $ip)->first(); $byIpId = $byIp ? data_get($byIp->config, 'mqtt_prefix') : null; if ($byIp !== null && (blank($byIpId) || $byIpId === $shellyId)) { $device = $byIp; } } $device ??= 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; } }