feat(call-home): Implement Auto Call Home configuration management
- Added `CallHomeConfig` model to represent the Auto Call Home settings. - Introduced methods in `MiniMateClient` for reading (`get_call_home_config`) and writing (`set_call_home_config`) the call home configuration. - Updated `MiniMateProtocol` with new commands for call home operations (SUB 0x2C for read, SUB 0x7E for write, and SUB 0x7F for confirm). - Created API endpoints for retrieving and updating call home settings in the server. - Enhanced the web interface with a new "Call Home" tab for user interaction with call home settings. - Implemented JavaScript functions for reading and writing call home configurations from the web app.
This commit is contained in:
+160
-1
@@ -59,7 +59,7 @@ except ImportError:
|
||||
|
||||
from minimateplus import MiniMateClient
|
||||
from minimateplus.protocol import ProtocolError
|
||||
from minimateplus.models import ComplianceConfig, DeviceInfo, Event, PeakValues, ProjectInfo, Timestamp
|
||||
from minimateplus.models import CallHomeConfig, ComplianceConfig, DeviceInfo, Event, PeakValues, ProjectInfo, Timestamp
|
||||
from minimateplus.transport import TcpTransport, DEFAULT_TCP_PORT
|
||||
from sfm.cache import SFMCache, get_cache
|
||||
from sfm.database import SeismoDb
|
||||
@@ -302,6 +302,27 @@ def _serialise_compliance_config(cc: Optional["ComplianceConfig"]) -> Optional[d
|
||||
}
|
||||
|
||||
|
||||
def _serialise_call_home_config(ch: Optional["CallHomeConfig"]) -> Optional[dict]:
|
||||
if ch is None:
|
||||
return None
|
||||
return {
|
||||
"auto_call_home_enabled": ch.auto_call_home_enabled,
|
||||
"dial_string": ch.dial_string,
|
||||
"after_event_recorded": ch.after_event_recorded,
|
||||
"at_specified_times": ch.at_specified_times,
|
||||
"time1_enabled": ch.time1_enabled,
|
||||
"time1_hour": ch.time1_hour,
|
||||
"time1_min": ch.time1_min,
|
||||
"time2_enabled": ch.time2_enabled,
|
||||
"time2_hour": ch.time2_hour,
|
||||
"time2_min": ch.time2_min,
|
||||
"num_retries": ch.num_retries,
|
||||
"time_between_retries_sec": ch.time_between_retries_sec,
|
||||
"wait_for_connection_sec": ch.wait_for_connection_sec,
|
||||
"warm_up_time_sec": ch.warm_up_time_sec,
|
||||
}
|
||||
|
||||
|
||||
def _serialise_device_info(info: DeviceInfo) -> dict:
|
||||
return {
|
||||
"serial": info.serial,
|
||||
@@ -1075,6 +1096,144 @@ def device_monitor_stop(
|
||||
return {"status": "stopped"}
|
||||
|
||||
|
||||
# ── Call home config endpoints ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.get("/device/call_home")
|
||||
def device_call_home_get(
|
||||
port: Optional[str] = Query(None, description="Serial port (e.g. COM5)"),
|
||||
baud: int = Query(38400, description="Serial baud rate"),
|
||||
host: Optional[str] = Query(None, description="TCP host — modem IP or ACH relay"),
|
||||
tcp_port: int = Query(DEFAULT_TCP_PORT, description=f"TCP port (default {DEFAULT_TCP_PORT})"),
|
||||
) -> dict:
|
||||
"""
|
||||
Read the Auto Call Home (ACH) configuration from the device.
|
||||
|
||||
Sends SUB 0x2C (two-step read) and returns the decoded call home config.
|
||||
|
||||
Confirmed from 4-20-26 call home settings captures (BE11529).
|
||||
|
||||
Returns:
|
||||
{
|
||||
"auto_call_home_enabled": true/false,
|
||||
"dial_string": "RADIO RING",
|
||||
"after_event_recorded": true/false,
|
||||
"at_specified_times": true/false,
|
||||
"time1_enabled": true/false, "time1_hour": 19, "time1_min": 55,
|
||||
"time2_enabled": false, "time2_hour": 0, "time2_min": 0,
|
||||
"num_retries": 3,
|
||||
"time_between_retries_sec": 15,
|
||||
"wait_for_connection_sec": 60,
|
||||
"warm_up_time_sec": 60
|
||||
}
|
||||
"""
|
||||
try:
|
||||
def _do():
|
||||
with _build_client(port, baud, host, tcp_port) as client:
|
||||
client.poll()
|
||||
return client.get_call_home_config()
|
||||
ch_config = _run_with_retry(_do, is_tcp=_is_tcp(host))
|
||||
except HTTPException:
|
||||
raise
|
||||
except ProtocolError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Protocol error: {exc}") from exc
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Connection error: {exc}") from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Device error: {exc}") from exc
|
||||
|
||||
return _serialise_call_home_config(ch_config) or {}
|
||||
|
||||
|
||||
class CallHomeConfigBody(BaseModel):
|
||||
"""
|
||||
Request body for POST /device/call_home.
|
||||
|
||||
All fields are optional — only supplied (non-null) fields are modified.
|
||||
All other call home config bytes are round-tripped verbatim from the device.
|
||||
|
||||
Confirmed writable fields (4-20-26 captures):
|
||||
auto_call_home_enabled : bool — master enable for auto call home
|
||||
after_event_recorded : bool — call home after each triggered event
|
||||
at_specified_times : bool — enable time-based scheduled calls
|
||||
time1_enabled : bool — enable time slot 1
|
||||
time1_hour : int — hour for slot 1 (0-23; avoid 3 — DLE escape limitation)
|
||||
time1_min : int — minute for slot 1 (0-59; avoid 3)
|
||||
time2_enabled : bool — enable time slot 2
|
||||
time2_hour : int — hour for slot 2 (0-23; avoid 3)
|
||||
time2_min : int — minute for slot 2 (0-59; avoid 3)
|
||||
|
||||
Read-only fields (not writable via this endpoint):
|
||||
dial_string, num_retries, time_between_retries_sec,
|
||||
wait_for_connection_sec, warm_up_time_sec
|
||||
"""
|
||||
auto_call_home_enabled: Optional[bool] = None
|
||||
after_event_recorded: Optional[bool] = None
|
||||
at_specified_times: Optional[bool] = None
|
||||
time1_enabled: Optional[bool] = None
|
||||
time1_hour: Optional[int] = None
|
||||
time1_min: Optional[int] = None
|
||||
time2_enabled: Optional[bool] = None
|
||||
time2_hour: Optional[int] = None
|
||||
time2_min: Optional[int] = None
|
||||
|
||||
|
||||
@app.post("/device/call_home")
|
||||
def device_call_home_set(
|
||||
body: CallHomeConfigBody,
|
||||
port: Optional[str] = Query(None, description="Serial port (e.g. COM5)"),
|
||||
baud: int = Query(38400, description="Serial baud rate"),
|
||||
host: Optional[str] = Query(None, description="TCP host — modem IP or ACH relay"),
|
||||
tcp_port: int = Query(DEFAULT_TCP_PORT, description=f"TCP port (default {DEFAULT_TCP_PORT})"),
|
||||
) -> dict:
|
||||
"""
|
||||
Read the current call home config, apply supplied changes, and write back.
|
||||
|
||||
Only non-null fields are modified. All other bytes round-trip verbatim.
|
||||
|
||||
Write sequence (confirmed from 4-20-26 call home settings captures):
|
||||
SUB 0x2C (read 2-step) → 125-byte raw payload
|
||||
patch fields
|
||||
SUB 0x7E (write 127-byte payload) → ack 0x81
|
||||
SUB 0x7F (confirm) → ack 0x80
|
||||
|
||||
Example body:
|
||||
{ "auto_call_home_enabled": true, "after_event_recorded": true,
|
||||
"time1_enabled": true, "time1_hour": 20, "time1_min": 0 }
|
||||
"""
|
||||
changed = body.model_dump(exclude_none=True)
|
||||
log.info("POST /device/call_home port=%s host=%s fields=%s", port, host, list(changed.keys()))
|
||||
|
||||
try:
|
||||
def _do():
|
||||
with _build_client(port, baud, host, tcp_port) as client:
|
||||
client.poll()
|
||||
client.set_call_home_config(
|
||||
auto_call_home_enabled=body.auto_call_home_enabled,
|
||||
after_event_recorded=body.after_event_recorded,
|
||||
at_specified_times=body.at_specified_times,
|
||||
time1_enabled=body.time1_enabled,
|
||||
time1_hour=body.time1_hour,
|
||||
time1_min=body.time1_min,
|
||||
time2_enabled=body.time2_enabled,
|
||||
time2_hour=body.time2_hour,
|
||||
time2_min=body.time2_min,
|
||||
)
|
||||
_run_with_retry(_do, is_tcp=_is_tcp(host))
|
||||
except HTTPException:
|
||||
raise
|
||||
except ProtocolError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Protocol error: {exc}") from exc
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Connection error: {exc}") from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Device error: {exc}") from exc
|
||||
|
||||
return {"status": "ok", "updated_fields": changed}
|
||||
|
||||
|
||||
# ── Cache management endpoints ────────────────────────────────────────────────
|
||||
|
||||
@app.get("/cache/stats")
|
||||
|
||||
Reference in New Issue
Block a user