mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-06-21 10:35:13 +02:00
6295f0fce1
- Introduced `_prefs_bytes_from_json` to convert JSON hex strings back to bytes for NodePrefs fields. - Updated `_load_prefs` logic to handle bytes conversion when loading preferences. - Improved the handling of preferences in the `RepeaterCompanionBridge` class to ensure proper type restoration.
140 lines
5.3 KiB
Python
140 lines
5.3 KiB
Python
"""
|
|
Repeater CompanionBridge with SQLite-backed preference persistence.
|
|
|
|
Persists full NodePrefs as a JSON blob so companion settings (including
|
|
auto-add config) survive repeater restarts. Merge-on-load supports
|
|
schema evolution when NodePrefs gains or loses fields.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import logging
|
|
from enum import Enum
|
|
from typing import Any, Callable, Optional
|
|
|
|
from pymc_core.companion import CompanionBridge
|
|
|
|
logger = logging.getLogger("RepeaterCompanionBridge")
|
|
|
|
|
|
def _prefs_bytes_from_json(value: Any) -> bytes:
|
|
"""Restore a ``bytes`` NodePrefs field from JSON (hex string from :func:`_to_json_safe`)."""
|
|
if value is None:
|
|
return b""
|
|
if isinstance(value, (bytes, bytearray)):
|
|
return bytes(value)
|
|
if isinstance(value, str):
|
|
s = value.strip()
|
|
if not s:
|
|
return b""
|
|
try:
|
|
return bytes.fromhex(s)
|
|
except ValueError:
|
|
logger.debug("Invalid hex for prefs bytes field (prefix %r)", s[:32])
|
|
return b""
|
|
return b""
|
|
|
|
|
|
def _to_json_safe(value: Any) -> Any:
|
|
"""Convert a value to a JSON-serializable form (avoids TypeError from enums, bytes, etc.)."""
|
|
if value is None or isinstance(value, (bool, int, float, str)):
|
|
return value
|
|
if isinstance(value, Enum):
|
|
return value.value
|
|
if isinstance(value, bytes):
|
|
return value.hex()
|
|
if isinstance(value, (list, tuple)):
|
|
return [_to_json_safe(v) for v in value]
|
|
if isinstance(value, dict):
|
|
return {k: _to_json_safe(v) for k, v in value.items()}
|
|
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
return {f.name: _to_json_safe(getattr(value, f.name)) for f in dataclasses.fields(value)}
|
|
return value
|
|
|
|
|
|
class RepeaterCompanionBridge(CompanionBridge):
|
|
"""CompanionBridge that persists and loads prefs (full NodePrefs) via SQLite JSON blob."""
|
|
|
|
def __init__(
|
|
self,
|
|
identity,
|
|
packet_injector: Callable[..., Any],
|
|
node_name: str = "pyMC",
|
|
adv_type: int = 1,
|
|
max_contacts: int = 1000,
|
|
max_channels: int = 40,
|
|
offline_queue_size: int = 512,
|
|
radio_config: Optional[dict] = None,
|
|
authenticate_callback: Optional[Callable[..., tuple[bool, int]]] = None,
|
|
initial_contacts: Optional[Any] = None,
|
|
*,
|
|
sqlite_handler=None,
|
|
companion_hash: str = "",
|
|
on_prefs_saved: Optional[Callable[[str], None]] = None,
|
|
) -> None:
|
|
self._sqlite_handler = sqlite_handler
|
|
self._companion_hash = companion_hash
|
|
self._on_prefs_saved = on_prefs_saved
|
|
super().__init__(
|
|
identity=identity,
|
|
packet_injector=packet_injector,
|
|
node_name=node_name,
|
|
adv_type=adv_type,
|
|
max_contacts=max_contacts,
|
|
max_channels=max_channels,
|
|
offline_queue_size=offline_queue_size,
|
|
radio_config=radio_config,
|
|
authenticate_callback=authenticate_callback,
|
|
initial_contacts=initial_contacts,
|
|
)
|
|
|
|
def _save_prefs(self) -> None:
|
|
"""Persist full NodePrefs as JSON to SQLite."""
|
|
if not self._sqlite_handler or not self._companion_hash:
|
|
return
|
|
try:
|
|
prefs_dict = dataclasses.asdict(self.prefs)
|
|
prefs_safe = _to_json_safe(prefs_dict)
|
|
self._sqlite_handler.companion_save_prefs(str(self._companion_hash), prefs_safe)
|
|
if self._on_prefs_saved:
|
|
try:
|
|
self._on_prefs_saved(self.prefs.node_name)
|
|
except Exception as e:
|
|
logger.warning("Failed to sync node_name to config: %s", e)
|
|
except Exception as e:
|
|
logger.warning("Failed to persist companion prefs: %s", e)
|
|
|
|
def _load_prefs(self) -> None:
|
|
"""Load prefs from SQLite JSON and merge into self.prefs (only known keys)."""
|
|
if not self._sqlite_handler or not self._companion_hash:
|
|
return
|
|
try:
|
|
stored = self._sqlite_handler.companion_load_prefs(self._companion_hash)
|
|
if not stored or not isinstance(stored, dict):
|
|
return
|
|
for key, value in stored.items():
|
|
if not hasattr(self.prefs, key):
|
|
continue
|
|
current = getattr(self.prefs, key)
|
|
try:
|
|
if value is None:
|
|
continue
|
|
if isinstance(current, bytes):
|
|
setattr(self.prefs, key, _prefs_bytes_from_json(value))
|
|
continue
|
|
if isinstance(current, bool):
|
|
setattr(self.prefs, key, bool(value))
|
|
elif isinstance(current, int):
|
|
setattr(self.prefs, key, int(value))
|
|
elif isinstance(current, float):
|
|
setattr(self.prefs, key, float(value))
|
|
elif isinstance(current, str):
|
|
setattr(self.prefs, key, str(value))
|
|
else:
|
|
setattr(self.prefs, key, value)
|
|
except (TypeError, ValueError) as e:
|
|
logger.debug("Skip prefs key %r: %s", key, e)
|
|
except Exception as e:
|
|
logger.warning("Failed to load companion prefs: %s", e)
|