"""Fleet projects Node-owned sensor state and forwards SDK operations, never hardware calls.""" import json import time from datetime import UTC, datetime from missioncore_plugin_sdk.v0alpha2.operations import OperationRequest from missioncore_plugin_sdk.v0alpha2.session import DeviceSessionSnapshot from .trust import PairingError ACTIONS = { "prepare", "details", "rename", "verify", "start", "replay", "stop", "option", "offer", "close-peer", } def validate_inventory(value, node_id): items = value.get("devices", []) if not isinstance(items, list) or len(items) > 16: raise ValueError("Invalid device inventory") seen = set() for item in items: snapshot = DeviceSessionSnapshot.model_validate(item["snapshot"]) if ( snapshot.context.execution.node_id != node_id or snapshot.context.device.device_id != item["id"] or item["id"] in seen ): raise ValueError("Invalid device execution binding") seen.add(item["id"]) state = value.get("sensor_state", {"items": items, "operations": [], "preparation": None}) if state.get("items") != items or len(json.dumps(state)) > 262144: raise ValueError("Invalid sensor state") return state def submit(fleet, vehicle_id, value): command = OperationRequest.model_validate(value) if ( command.action_id not in ACTIONS or command.idempotency_key != command.operation_id or len(json.dumps(value)) > 65536 ): raise PairingError("Неподдерживаемая команда устройства.") with fleet.lock: row = fleet.find(vehicle_id) if fleet.public(row)["connectivity"] != "online": raise PairingError("БК недоступен. Дождитесь связи перед настройкой устройства.") existing = row.setdefault("sensor_commands", {}).get(command.operation_id) if existing: if existing["command"] != value: raise PairingError("Идентификатор операции уже использован.") return existing if ( not datetime.now(UTC) < command.deadline_at or (command.deadline_at - command.requested_at).total_seconds() > 360 ): raise PairingError("Срок команды истёк. Обновите устройство.") item = next( ( item for item in row.get("sensor_state", {}).get("items", []) if item["id"] == command.session.device_id ), None, ) if item is None or item["snapshot"]["context"]["session_id"] != command.session.session_id: raise PairingError("Сеанс устройства изменился. Обновите сведения.") row["sensor_commands"] = { k: v for k, v in row["sensor_commands"].items() if time.time() - v["updated_at"] < 600 } if len(row["sensor_commands"]) >= 32: raise PairingError("Слишком много запросов к устройству. Повторите позже.") operation = {"command": value, "state": "queued", "updated_at": time.time()} row["sensor_commands"][command.operation_id] = operation fleet.save(row) return operation def operation(fleet, vehicle_id, identifier): with fleet.lock: value = fleet.find(vehicle_id).get("sensor_commands", {}).get(identifier) if value is None: raise PairingError("Операция не найдена.") if value["state"] in ("queued", "running") and datetime.fromisoformat( value["command"]["deadline_at"] ) < datetime.now(UTC): return { **value, "state": "unknown", "error": "Подтверждение не получено. Обновите фактическое состояние устройства.", } return value def heartbeat(row, value): commands = row.setdefault("sensor_commands", {}) acknowledgements = [] results = value.get("sensor_results", []) if not isinstance(results, list) or len(results) > 64: raise ValueError("Invalid sensor operations") for result in results: command = result.get("command", {}) identifier = command.get("operation_id") existing = commands.get(identifier) if ( existing and existing["command"] == command and result.get("state") in ("running", "complete", "error", "unknown") ): commands[identifier] = {**result, "updated_at": time.time()} if result["state"] != "running": acknowledgements.append(identifier) pending = [] for entry in commands.values(): if entry["state"] == "queued" and datetime.fromisoformat( entry["command"]["deadline_at"] ) > datetime.now(UTC): pending.append(entry["command"]) return {"sensor_commands": pending[:4], "sensor_acknowledgements": acknowledgements}