Upload
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""Server announce and pairing polling client."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from core.identity import compute_fingerprint
|
||||
from core.pairing import PairingManager, PairingState
|
||||
from storage.secret_store import SecretStore
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnnounceConfig:
|
||||
server_url: str
|
||||
firmware_version: str
|
||||
capabilities: dict[str, Any]
|
||||
announce_interval_seconds: int = 30
|
||||
polling_interval_seconds: int = 10
|
||||
|
||||
|
||||
def build_payload(
|
||||
box_id: str,
|
||||
secret_seed: str,
|
||||
config: AnnounceConfig,
|
||||
) -> dict[str, Any]:
|
||||
fingerprint = compute_fingerprint(box_id, secret_seed)
|
||||
return {
|
||||
"box_id": box_id,
|
||||
"fingerprint": fingerprint,
|
||||
"firmware_version": config.firmware_version,
|
||||
"capabilities": config.capabilities,
|
||||
}
|
||||
|
||||
|
||||
def _post_json(url: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
body = resp.read().decode("utf-8")
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
return json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
async def announce_loop(
|
||||
box_id: str,
|
||||
secrets: SecretStore,
|
||||
pairing: PairingManager,
|
||||
config: AnnounceConfig,
|
||||
stop_event: asyncio.Event,
|
||||
) -> None:
|
||||
secret_seed = secrets.ensure_secret_seed()
|
||||
payload = build_payload(box_id, secret_seed, config)
|
||||
announce_url = f"{config.server_url}/api/boxes/announce"
|
||||
|
||||
while not stop_event.is_set():
|
||||
if not pairing.is_paired():
|
||||
await asyncio.to_thread(_post_json, announce_url, payload)
|
||||
await asyncio.sleep(config.announce_interval_seconds)
|
||||
|
||||
|
||||
async def pairing_poll_loop(
|
||||
box_id: str,
|
||||
pairing: PairingManager,
|
||||
config: AnnounceConfig,
|
||||
stop_event: asyncio.Event,
|
||||
) -> None:
|
||||
poll_url = f"{config.server_url}/api/boxes/poll-pairing"
|
||||
payload = {"box_id": box_id}
|
||||
while not stop_event.is_set():
|
||||
if pairing.is_paired():
|
||||
await asyncio.sleep(config.polling_interval_seconds)
|
||||
continue
|
||||
pairing.set_state(PairingState.PAIRING_PENDING)
|
||||
response = await asyncio.to_thread(_post_json, poll_url, payload)
|
||||
if response and response.get("paired") is True:
|
||||
token = response.get("api_token")
|
||||
if isinstance(token, str) and token:
|
||||
pairing.save_api_token(token)
|
||||
await asyncio.sleep(config.polling_interval_seconds)
|
||||
Reference in New Issue
Block a user