|
|
|
@@ -128,6 +128,7 @@ XGRIDS_K1_COMPATIBILITY_PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.local-network
|
|
|
|
|
DEFAULT_ACQUISITION_CAMERA_SOURCE: CameraSourceId = "sensor.camera.right"
|
|
|
|
|
CONTROL_MQTT_PORT = 1883
|
|
|
|
|
CONTROL_ENDPOINT_PROBE_TIMEOUT_SECONDS = 1.5
|
|
|
|
|
BLE_DISCOVERY_LEASE_TTL_SECONDS = 60.0
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
@@ -277,6 +278,28 @@ class ConnectRequest(StrictRequest):
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConnectionVerifyRequest(StrictRequest):
|
|
|
|
|
"""Refresh one lease or adopt a scanned K1 already on the direct LAN."""
|
|
|
|
|
|
|
|
|
|
device_id: str | None = Field(default=None, min_length=1, max_length=128)
|
|
|
|
|
compatibility_attestation: CompatibilityAttestationRequest | None = None
|
|
|
|
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
def validate_adoption_request(self) -> Self:
|
|
|
|
|
has_device = self.device_id is not None
|
|
|
|
|
has_attestation = self.compatibility_attestation is not None
|
|
|
|
|
if has_device != has_attestation:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"device_id and compatibility_attestation must be provided together"
|
|
|
|
|
)
|
|
|
|
|
if (
|
|
|
|
|
self.compatibility_attestation is not None
|
|
|
|
|
and self.compatibility_attestation.topology != "direct-lan"
|
|
|
|
|
):
|
|
|
|
|
raise ValueError("connection.verify adoption requires topology=direct-lan")
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LiveRequest(StrictRequest):
|
|
|
|
|
project_name: str = Field(min_length=1, max_length=96)
|
|
|
|
|
host: str | None = Field(default=None, max_length=15)
|
|
|
|
@@ -422,6 +445,8 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
self._provisioning_active = False
|
|
|
|
|
self._fingerprint_key = secrets.token_bytes(32)
|
|
|
|
|
self._devices: list[dict[str, Any]] = []
|
|
|
|
|
self._ble_discovery_generation = 0
|
|
|
|
|
self._ble_device_last_seen_monotonic: dict[str, float] = {}
|
|
|
|
|
self._selected_device_id: str | None = None
|
|
|
|
|
self._k1_ip: str | None = None
|
|
|
|
|
self._connection_mode: ConnectionMode | None = None
|
|
|
|
@@ -494,6 +519,25 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
allow_device_ap=connection_mode == "quick-connect",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _fresh_ble_devices_locked(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
now_monotonic: float | None = None,
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
"""Return only candidates leased by the current in-memory scan generation."""
|
|
|
|
|
|
|
|
|
|
now = time.monotonic() if now_monotonic is None else now_monotonic
|
|
|
|
|
fresh: list[dict[str, Any]] = []
|
|
|
|
|
for item in self._devices:
|
|
|
|
|
device_id = str(item.get("device_id") or "")
|
|
|
|
|
last_seen = self._ble_device_last_seen_monotonic.get(device_id)
|
|
|
|
|
if last_seen is None:
|
|
|
|
|
continue
|
|
|
|
|
age_seconds = now - last_seen
|
|
|
|
|
if 0.0 <= age_seconds <= BLE_DISCOVERY_LEASE_TTL_SECONDS:
|
|
|
|
|
fresh.append(dict(item))
|
|
|
|
|
return fresh
|
|
|
|
|
|
|
|
|
|
@_serialized_acquisition_access
|
|
|
|
|
def state(self) -> dict[str, Any]:
|
|
|
|
|
application_control = self._application_control.snapshot().as_dict()
|
|
|
|
@@ -507,7 +551,8 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
with self._lock:
|
|
|
|
|
operation_phase = self._operation_phase
|
|
|
|
|
operation_message = self._operation_message
|
|
|
|
|
devices = list(self._devices)
|
|
|
|
|
devices = self._fresh_ble_devices_locked()
|
|
|
|
|
discovery_stale = bool(self._devices) and not devices
|
|
|
|
|
selected_device_id = self._selected_device_id
|
|
|
|
|
k1_ip = self._k1_ip
|
|
|
|
|
connection_mode = self._connection_mode
|
|
|
|
@@ -572,6 +617,9 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
f"Найдено BLE-устройств: {len(devices)}. "
|
|
|
|
|
f"Совместимых профилей: {likely_count}. Выберите нужное устройство."
|
|
|
|
|
)
|
|
|
|
|
elif discovery_stale:
|
|
|
|
|
phase = "idle"
|
|
|
|
|
message = "Результаты Bluetooth-поиска устарели. Выполните поиск ещё раз."
|
|
|
|
|
else:
|
|
|
|
|
phase = "idle"
|
|
|
|
|
message = runtime["message"]
|
|
|
|
@@ -686,6 +734,15 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async def scan_ble(self, duration_seconds: float) -> dict[str, Any]:
|
|
|
|
|
with self._lock:
|
|
|
|
|
if self._provisioning_active:
|
|
|
|
|
raise RuntimeError("нельзя запускать BLE-поиск во время настройки Wi-Fi")
|
|
|
|
|
self._ble_discovery_generation += 1
|
|
|
|
|
scan_generation = self._ble_discovery_generation
|
|
|
|
|
# Discovery results are an in-memory lease, not a durable catalog.
|
|
|
|
|
# A new explicit scan invalidates the previous generation before I/O.
|
|
|
|
|
self._devices = []
|
|
|
|
|
self._ble_device_last_seen_monotonic = {}
|
|
|
|
|
operation, _ = self._operations.begin(
|
|
|
|
|
ACTION_DISCOVERY_SCAN,
|
|
|
|
|
deadline_seconds=duration_seconds + 10.0,
|
|
|
|
@@ -710,9 +767,16 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
}
|
|
|
|
|
for item in result["devices"]
|
|
|
|
|
]
|
|
|
|
|
observed_monotonic = time.monotonic()
|
|
|
|
|
with self._lock:
|
|
|
|
|
self._devices = devices
|
|
|
|
|
self._operation_message = f"Поиск завершён. Найдено BLE-устройств: {len(devices)}."
|
|
|
|
|
if self._ble_discovery_generation == scan_generation:
|
|
|
|
|
self._devices = devices
|
|
|
|
|
self._ble_device_last_seen_monotonic = {
|
|
|
|
|
str(item["device_id"]): observed_monotonic for item in devices
|
|
|
|
|
}
|
|
|
|
|
self._operation_message = (
|
|
|
|
|
f"Поиск завершён. Найдено BLE-устройств: {len(devices)}."
|
|
|
|
|
)
|
|
|
|
|
self._operations.transition(
|
|
|
|
|
operation.operation_id,
|
|
|
|
|
"succeeded",
|
|
|
|
@@ -721,6 +785,10 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
result={"candidate_count": len(devices)},
|
|
|
|
|
)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
with self._lock:
|
|
|
|
|
if self._ble_discovery_generation == scan_generation:
|
|
|
|
|
self._devices = []
|
|
|
|
|
self._ble_device_last_seen_monotonic = {}
|
|
|
|
|
self._operations.transition(
|
|
|
|
|
operation.operation_id,
|
|
|
|
|
"failed",
|
|
|
|
@@ -731,16 +799,20 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
raise
|
|
|
|
|
finally:
|
|
|
|
|
with self._lock:
|
|
|
|
|
self._operation_phase = None
|
|
|
|
|
if self._ble_discovery_generation == scan_generation:
|
|
|
|
|
self._operation_phase = None
|
|
|
|
|
return self.state()
|
|
|
|
|
|
|
|
|
|
async def connect(self, request: ConnectRequest) -> dict[str, Any]:
|
|
|
|
|
known_ids = {str(item["device_id"]) for item in self.state()["devices"]}
|
|
|
|
|
with self._lock:
|
|
|
|
|
scanned_devices = self._fresh_ble_devices_locked()
|
|
|
|
|
discovery_generation = self._ble_discovery_generation
|
|
|
|
|
known_ids = {str(item["device_id"]) for item in scanned_devices}
|
|
|
|
|
if request.device_id not in known_ids:
|
|
|
|
|
raise ValueError("сначала найдите и выберите устройство через Bluetooth")
|
|
|
|
|
quick_connect = request.connection_mode == "quick-connect"
|
|
|
|
|
selected_device = next(
|
|
|
|
|
item for item in self.state()["devices"] if item["device_id"] == request.device_id
|
|
|
|
|
item for item in scanned_devices if item["device_id"] == request.device_id
|
|
|
|
|
)
|
|
|
|
|
selected_device_name = str(selected_device.get("name") or "").strip()
|
|
|
|
|
control_snapshot = self._application_control_session.snapshot()
|
|
|
|
@@ -860,6 +932,18 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
raise HostWifiProfileError("credential-source-unavailable")
|
|
|
|
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
if (
|
|
|
|
|
self._ble_discovery_generation != discovery_generation
|
|
|
|
|
or request.device_id
|
|
|
|
|
not in {
|
|
|
|
|
str(item["device_id"])
|
|
|
|
|
for item in self._fresh_ble_devices_locked()
|
|
|
|
|
}
|
|
|
|
|
):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"результаты Bluetooth-поиска изменились или устарели; "
|
|
|
|
|
"выполните поиск ещё раз"
|
|
|
|
|
)
|
|
|
|
|
active_acquisition = self._acquisition
|
|
|
|
|
if (
|
|
|
|
|
active_acquisition is not None
|
|
|
|
@@ -1142,21 +1226,41 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
evidence_refs=(f"evidence-session-{session_dir.name}",),
|
|
|
|
|
)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
operation_error = _operation_error(
|
|
|
|
|
exc,
|
|
|
|
|
category="transport" if quick_connect else "device",
|
|
|
|
|
side_effect_status=("unknown" if network_change_attempted else "none"),
|
|
|
|
|
safe_to_retry=not network_change_attempted,
|
|
|
|
|
)
|
|
|
|
|
self._operations.transition_if_pending(
|
|
|
|
|
operation.operation_id,
|
|
|
|
|
"failed",
|
|
|
|
|
stage_code=f"{operation_stage}-failed",
|
|
|
|
|
message_code="network.provision.failed",
|
|
|
|
|
error=_operation_error(
|
|
|
|
|
exc,
|
|
|
|
|
category="transport" if quick_connect else "device",
|
|
|
|
|
side_effect_status=("unknown" if network_change_attempted else "none"),
|
|
|
|
|
safe_to_retry=not network_change_attempted,
|
|
|
|
|
),
|
|
|
|
|
error=operation_error,
|
|
|
|
|
evidence_refs=(
|
|
|
|
|
(f"evidence-session-{session_dir.name}",) if session_dir is not None else ()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
# Never log the request, SSID, password, BLE payload, or exception
|
|
|
|
|
# text here: lower transport errors may embed sensitive frame data.
|
|
|
|
|
# The structured record is sufficient to correlate the HTTP 502
|
|
|
|
|
# with its operation stage and retry/side-effect safety contract.
|
|
|
|
|
logger.error(
|
|
|
|
|
"K1 network provisioning failed",
|
|
|
|
|
extra={
|
|
|
|
|
"event_code": "k1_network_provision_failed",
|
|
|
|
|
"operation_id": operation.operation_id,
|
|
|
|
|
"operation_stage": operation_stage,
|
|
|
|
|
"connection_mode": request.connection_mode,
|
|
|
|
|
"error_category": operation_error["category"],
|
|
|
|
|
"error_code": operation_error["code"],
|
|
|
|
|
"safe_to_retry": operation_error["safe_to_retry"],
|
|
|
|
|
"side_effect_status": operation_error["side_effect_status"],
|
|
|
|
|
"network_change_attempted": network_change_attempted,
|
|
|
|
|
"automatic_retry": False,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
raise
|
|
|
|
|
finally:
|
|
|
|
|
password = ""
|
|
|
|
@@ -1216,12 +1320,169 @@ class XgridsK1CompatibilityService:
|
|
|
|
|
payload=segment.payload,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def verify_connection(self) -> dict[str, Any]:
|
|
|
|
|
"""Refresh the session-scoped DHCP address from the read-only BLE status."""
|
|
|
|
|
def verify_connection(
|
|
|
|
|
self,
|
|
|
|
|
request: ConnectionVerifyRequest | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Refresh a lease or adopt an externally configured direct-LAN K1."""
|
|
|
|
|
|
|
|
|
|
self._refresh_live_lan_address(rediscover=True)
|
|
|
|
|
request = request or ConnectionVerifyRequest()
|
|
|
|
|
if request.device_id is None:
|
|
|
|
|
self._refresh_live_lan_address(rediscover=True)
|
|
|
|
|
else:
|
|
|
|
|
assert request.compatibility_attestation is not None
|
|
|
|
|
self._adopt_existing_lan_connection(
|
|
|
|
|
request.device_id,
|
|
|
|
|
request.compatibility_attestation,
|
|
|
|
|
)
|
|
|
|
|
return self.state()
|
|
|
|
|
|
|
|
|
|
def _adopt_existing_lan_connection(
|
|
|
|
|
self,
|
|
|
|
|
device_id: str,
|
|
|
|
|
compatibility_attestation: CompatibilityAttestationRequest,
|
|
|
|
|
) -> str:
|
|
|
|
|
"""Create a process lease from BLE status without changing K1 Wi-Fi state."""
|
|
|
|
|
|
|
|
|
|
if compatibility_attestation.topology != "direct-lan":
|
|
|
|
|
raise ValueError("read-only K1 adoption supports only direct-lan topology")
|
|
|
|
|
with self._lock:
|
|
|
|
|
scanned_device = next(
|
|
|
|
|
(
|
|
|
|
|
dict(item)
|
|
|
|
|
for item in self._fresh_ble_devices_locked()
|
|
|
|
|
if str(item.get("device_id")) == device_id
|
|
|
|
|
),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
discovery_generation = self._ble_discovery_generation
|
|
|
|
|
acquisition = self._acquisition
|
|
|
|
|
acquisition_session_lease = self._acquisition_session_lease
|
|
|
|
|
provisioning_active = self._provisioning_active
|
|
|
|
|
previous_target = self._k1_ip
|
|
|
|
|
if scanned_device is None:
|
|
|
|
|
raise ValueError("сначала найдите и выберите устройство через Bluetooth")
|
|
|
|
|
if scanned_device.get("connectable") is False:
|
|
|
|
|
raise ValueError("выбранное Bluetooth-устройство сейчас недоступно для подключения")
|
|
|
|
|
if provisioning_active:
|
|
|
|
|
raise RuntimeError("нельзя проверять существующую сеть во время настройки Wi-Fi")
|
|
|
|
|
if acquisition is not None and acquisition.state not in TERMINAL_ACQUISITION_STATES:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"нельзя менять connection lease во время активной acquisition-сессии"
|
|
|
|
|
)
|
|
|
|
|
if acquisition_session_lease is not None:
|
|
|
|
|
raise RuntimeError("предыдущая evidence-сессия ещё не остановлена и не запечатана")
|
|
|
|
|
runtime_state = self.runtime.snapshot()
|
|
|
|
|
if runtime_state.get("source_mode") != "idle":
|
|
|
|
|
raise RuntimeError("нельзя менять connection lease при активном live/replay источнике")
|
|
|
|
|
|
|
|
|
|
if not self._provisioning_gate.acquire(blocking=False):
|
|
|
|
|
raise RuntimeError("другая операция настройки или проверки сети уже выполняется")
|
|
|
|
|
try:
|
|
|
|
|
status_read = asyncio.run(
|
|
|
|
|
read_wifi_status_once(
|
|
|
|
|
device_id,
|
|
|
|
|
timeout_seconds=20.0,
|
|
|
|
|
rediscover=True,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if (
|
|
|
|
|
status_read.get("operation") != "single_reviewed_wifi_status_read"
|
|
|
|
|
or status_read.get("write_performed") is not False
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("BLE status read не подтвердил read-only операцию")
|
|
|
|
|
observed_target = status_read["status"]["ipv4"]
|
|
|
|
|
if observed_target is None or observed_target == AP_FALLBACK_IPV4:
|
|
|
|
|
raise RuntimeError("K1 не сообщил актуальный DHCP-адрес общей сети")
|
|
|
|
|
target = validate_private_ipv4(observed_target)
|
|
|
|
|
if _target_is_local_ipv4(target):
|
|
|
|
|
raise RuntimeError("BLE status сообщил адрес, принадлежащий этому компьютеру")
|
|
|
|
|
host_route_class = _host_route_class(target)
|
|
|
|
|
if host_route_class in {"default-route", "tunnel"}:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"K1 сообщил адрес другой сети; прямой локальный маршрут отсутствует"
|
|
|
|
|
)
|
|
|
|
|
if not _control_endpoint_reachable(target):
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"K1 сообщил адрес общей сети, но MQTT endpoint 1883 недоступен"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Retire only local, terminal control ownership after every external
|
|
|
|
|
# observation has passed. No scanner command is sent by this call.
|
|
|
|
|
self._application_control_session.retire_for_network_change()
|
|
|
|
|
observed_at = str(status_read["observed_at_utc"])
|
|
|
|
|
with self._lock:
|
|
|
|
|
if self._provisioning_active:
|
|
|
|
|
raise RuntimeError("настройка Wi-Fi началась во время проверки сети")
|
|
|
|
|
if self._ble_discovery_generation != discovery_generation or not any(
|
|
|
|
|
str(item.get("device_id")) == device_id for item in self._devices
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("список найденных Bluetooth-устройств изменился")
|
|
|
|
|
current_acquisition = self._acquisition
|
|
|
|
|
if (
|
|
|
|
|
current_acquisition is not None
|
|
|
|
|
and current_acquisition.state not in TERMINAL_ACQUISITION_STATES
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"acquisition-сессия началась во время проверки существующей сети"
|
|
|
|
|
)
|
|
|
|
|
if self._acquisition_session_lease is not None:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"evidence-сессия изменилась во время проверки существующей сети"
|
|
|
|
|
)
|
|
|
|
|
persistent_device_id = self._device_ids_by_transport_ref.get(device_id)
|
|
|
|
|
if persistent_device_id is None:
|
|
|
|
|
persistent_device_id = new_device_id()
|
|
|
|
|
self._device_ids_by_transport_ref[device_id] = persistent_device_id
|
|
|
|
|
self._selected_device_id = device_id
|
|
|
|
|
self._k1_ip = target
|
|
|
|
|
self._connection_mode = "bridge"
|
|
|
|
|
self._device_id = persistent_device_id
|
|
|
|
|
self._device_session_id = new_device_session_id()
|
|
|
|
|
self._device_session_opened_at = observed_at
|
|
|
|
|
self._connection_lease_generation += 1
|
|
|
|
|
self._compatibility_attestation = _attestation_snapshot(
|
|
|
|
|
compatibility_attestation
|
|
|
|
|
)
|
|
|
|
|
self._device_calibration = unavailable_device_calibration_snapshot(
|
|
|
|
|
XGRIDS_K1_COMPATIBILITY_PROFILE_ID
|
|
|
|
|
)
|
|
|
|
|
self._connection_verification = {
|
|
|
|
|
"status": "adopted",
|
|
|
|
|
"lease_state": "reachable",
|
|
|
|
|
"lease_generation": self._connection_lease_generation,
|
|
|
|
|
"endpoint_validation": "ble-wifi-status-read+mqtt-tcp-connect",
|
|
|
|
|
"network_reachability": "reachable",
|
|
|
|
|
"host_route_class": host_route_class,
|
|
|
|
|
"address_source": "ble-wifi-status-read",
|
|
|
|
|
"connection_origin": "external-existing-network",
|
|
|
|
|
"admission_source": ACTION_CONNECTION_VERIFY,
|
|
|
|
|
"address_changed": previous_target != target,
|
|
|
|
|
"previous_address_present": previous_target is not None,
|
|
|
|
|
"write_performed": False,
|
|
|
|
|
"observed_at": observed_at,
|
|
|
|
|
}
|
|
|
|
|
self._operation_message = (
|
|
|
|
|
"K1 найден в существующей локальной сети без изменения его настроек."
|
|
|
|
|
)
|
|
|
|
|
lease_generation = self._connection_lease_generation
|
|
|
|
|
logger.info(
|
|
|
|
|
"K1 existing direct-LAN connection adopted without provisioning",
|
|
|
|
|
extra={
|
|
|
|
|
"event_code": "k1_existing_lan_connection_adopted",
|
|
|
|
|
"lease_generation": lease_generation,
|
|
|
|
|
"connection_origin": "external-existing-network",
|
|
|
|
|
"address_source": "ble-wifi-status-read",
|
|
|
|
|
"host_route_class": host_route_class,
|
|
|
|
|
"endpoint_reachable": True,
|
|
|
|
|
"device_write_performed": False,
|
|
|
|
|
"automatic_retry": False,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return target
|
|
|
|
|
finally:
|
|
|
|
|
self._provisioning_gate.release()
|
|
|
|
|
|
|
|
|
|
def _refresh_live_lan_address(self, *, rediscover: bool = False) -> str:
|
|
|
|
|
with self._lock:
|
|
|
|
|
selected_device_id = self._selected_device_id
|
|
|
|
@@ -3156,7 +3417,10 @@ class XgridsK1ServicePort(Protocol):
|
|
|
|
|
|
|
|
|
|
def inspect_device(self) -> dict[str, Any]: ...
|
|
|
|
|
|
|
|
|
|
def verify_connection(self) -> dict[str, Any]: ...
|
|
|
|
|
def verify_connection(
|
|
|
|
|
self,
|
|
|
|
|
request: ConnectionVerifyRequest | None = None,
|
|
|
|
|
) -> dict[str, Any]: ...
|
|
|
|
|
|
|
|
|
|
def read_device_calibration_snapshot(self) -> dict[str, Any]: ...
|
|
|
|
|
|
|
|
|
@@ -3282,8 +3546,8 @@ class XgridsK1PluginFacade:
|
|
|
|
|
connect_request = ConnectRequest.model_validate(payload)
|
|
|
|
|
return await self.service.connect(connect_request)
|
|
|
|
|
if action_id == ACTION_CONNECTION_VERIFY:
|
|
|
|
|
EmptyRequest.model_validate(payload)
|
|
|
|
|
return await asyncio.to_thread(self.service.verify_connection)
|
|
|
|
|
verify_request = ConnectionVerifyRequest.model_validate(payload)
|
|
|
|
|
return await asyncio.to_thread(self.service.verify_connection, verify_request)
|
|
|
|
|
if action_id == ACTION_APPLICATION_CONTROL_SHADOW_STATE:
|
|
|
|
|
EmptyRequest.model_validate(payload)
|
|
|
|
|
return await asyncio.to_thread(self.service.state)
|
|
|
|
|