mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
The tare and calibrate buttons on the Settings page queued commands
but never executed them due to three broken links:
1. Daemon received tare command via heartbeat but never called
scale.tare() — the ScaleReader was available in shared dict
but unused
2. No API endpoint for the daemon to report the new tare offset
back to the backend DB, so tare results were lost
3. Heartbeat updated config but never called
scale.update_calibration(), so ScaleReader kept initial values
Added set-tare endpoint + API client method, and fixed heartbeat
loop to execute tare, persist the result, and propagate calibration
changes to the ScaleReader instance.
154 lines
4.9 KiB
Python
154 lines
4.9 KiB
Python
"""HTTP client for communicating with Bambuddy backend."""
|
|
|
|
import asyncio
|
|
import logging
|
|
from collections import deque
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MAX_BUFFER_SIZE = 100
|
|
|
|
|
|
class APIClient:
|
|
def __init__(self, backend_url: str, api_key: str):
|
|
self._base = backend_url.rstrip("/") + "/api/v1/spoolbuddy"
|
|
self._headers = {"X-API-Key": api_key} if api_key else {}
|
|
self._client = httpx.AsyncClient(timeout=10.0, headers=self._headers)
|
|
self._backoff = 1.0
|
|
self._max_backoff = 30.0
|
|
self._buffer: deque[dict] = deque(maxlen=MAX_BUFFER_SIZE)
|
|
self._connected = False
|
|
|
|
async def close(self):
|
|
await self._client.aclose()
|
|
|
|
async def _post(self, path: str, data: dict) -> dict | None:
|
|
try:
|
|
resp = await self._client.post(f"{self._base}{path}", json=data)
|
|
resp.raise_for_status()
|
|
self._backoff = 1.0
|
|
self._connected = True
|
|
return resp.json()
|
|
except Exception as e:
|
|
if self._connected:
|
|
logger.warning("Backend connection lost: %s", e)
|
|
self._connected = False
|
|
self._buffer.append({"path": path, "data": data})
|
|
return None
|
|
|
|
async def _get(self, path: str) -> dict | None:
|
|
try:
|
|
resp = await self._client.get(f"{self._base}{path}")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
except Exception as e:
|
|
logger.warning("GET %s failed: %s", path, e)
|
|
return None
|
|
|
|
async def _flush_buffer(self):
|
|
while self._buffer:
|
|
item = self._buffer[0]
|
|
try:
|
|
resp = await self._client.post(f"{self._base}{item['path']}", json=item["data"])
|
|
resp.raise_for_status()
|
|
self._buffer.popleft()
|
|
except Exception:
|
|
break
|
|
|
|
async def register_device(
|
|
self,
|
|
device_id: str,
|
|
hostname: str,
|
|
ip_address: str,
|
|
firmware_version: str | None = None,
|
|
has_nfc: bool = True,
|
|
has_scale: bool = True,
|
|
tare_offset: int = 0,
|
|
calibration_factor: float = 1.0,
|
|
) -> dict | None:
|
|
while True:
|
|
result = await self._post(
|
|
"/devices/register",
|
|
{
|
|
"device_id": device_id,
|
|
"hostname": hostname,
|
|
"ip_address": ip_address,
|
|
"firmware_version": firmware_version,
|
|
"has_nfc": has_nfc,
|
|
"has_scale": has_scale,
|
|
"tare_offset": tare_offset,
|
|
"calibration_factor": calibration_factor,
|
|
},
|
|
)
|
|
if result is not None:
|
|
logger.info("Registered with backend as %s", device_id)
|
|
return result
|
|
logger.warning("Registration failed, retrying in %.0fs...", self._backoff)
|
|
await asyncio.sleep(self._backoff)
|
|
self._backoff = min(self._backoff * 2, self._max_backoff)
|
|
|
|
async def heartbeat(
|
|
self, device_id: str, nfc_ok: bool, scale_ok: bool, uptime_s: int, ip_address: str | None = None
|
|
) -> dict | None:
|
|
result = await self._post(
|
|
f"/devices/{device_id}/heartbeat",
|
|
{
|
|
"nfc_ok": nfc_ok,
|
|
"scale_ok": scale_ok,
|
|
"uptime_s": uptime_s,
|
|
"ip_address": ip_address,
|
|
},
|
|
)
|
|
if result and self._buffer:
|
|
await self._flush_buffer()
|
|
return result
|
|
|
|
async def tag_scanned(
|
|
self,
|
|
device_id: str,
|
|
tag_uid: str,
|
|
tray_uuid: str | None = None,
|
|
sak: int | None = None,
|
|
tag_type: str | None = None,
|
|
) -> dict | None:
|
|
return await self._post(
|
|
"/nfc/tag-scanned",
|
|
{
|
|
"device_id": device_id,
|
|
"tag_uid": tag_uid,
|
|
"tray_uuid": tray_uuid,
|
|
"sak": sak,
|
|
"tag_type": tag_type,
|
|
},
|
|
)
|
|
|
|
async def tag_removed(self, device_id: str, tag_uid: str) -> dict | None:
|
|
return await self._post(
|
|
"/nfc/tag-removed",
|
|
{
|
|
"device_id": device_id,
|
|
"tag_uid": tag_uid,
|
|
},
|
|
)
|
|
|
|
async def update_tare(self, device_id: str, tare_offset: int) -> dict | None:
|
|
return await self._post(
|
|
f"/devices/{device_id}/calibration/set-tare",
|
|
{"tare_offset": tare_offset},
|
|
)
|
|
|
|
async def scale_reading(
|
|
self, device_id: str, weight_grams: float, stable: bool, raw_adc: int | None = None
|
|
) -> dict | None:
|
|
return await self._post(
|
|
"/scale/reading",
|
|
{
|
|
"device_id": device_id,
|
|
"weight_grams": weight_grams,
|
|
"stable": stable,
|
|
"raw_adc": raw_adc,
|
|
},
|
|
)
|