pdo->prepare('SELECT secret FROM move_devices WHERE device_id = ? LIMIT 1'); $st->execute([$deviceId]); $secret = $st->fetchColumn(); return $secret === false ? null : (string)$secret; } public function siteFor(string $deviceId): ?string { $st = $this->pdo->prepare('SELECT site FROM move_devices WHERE device_id = ? LIMIT 1'); $st->execute([$deviceId]); $site = $st->fetchColumn(); // false = no such device; null = device exists but unassigned. Both -> null (unscoped). return ($site === false || $site === null) ? null : (string)$site; } public function touchHeartbeat(string $deviceId, array $meta): void { // A plain UPDATE (not upsert) is safe here: the caller (HeartbeatController) // only reaches this after secretFor() already succeeded for the same // device_id, so the row is guaranteed to exist. Device rows are provisioned // out-of-band by an admin (see server/docs/server-deploy.md), never created // implicitly from an inbound heartbeat. $st = $this->pdo->prepare( 'UPDATE move_devices SET last_fps = ?, last_temp = ?, last_throttle = ?, reported_config_version = ?, last_heartbeat_at = ? WHERE device_id = ?' ); $st->execute([ (float)($meta['fps'] ?? 0.0), (float)($meta['temp'] ?? 0.0), !empty($meta['throttle']) ? 1 : 0, (int)($meta['config_version'] ?? 0), (int)($meta['at'] ?? time()), $deviceId, ]); } public function configVersion(string $deviceId): int { $st = $this->pdo->prepare('SELECT config_version FROM move_devices WHERE device_id = ? LIMIT 1'); $st->execute([$deviceId]); $version = $st->fetchColumn(); return $version === false ? 1 : (int)$version; } }