Files
IoTManagerWeb/scripts/mock_backend.py
DmitryBorisenko33 3ad761e6f2 optimization
2026-02-08 23:38:55 +01:00

611 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Mock backend for IoTManager frontend (dev mode).
Emulates devices over one WebSocket server (port 81) and HTTP for /iotm/ver.json.
Frontend flow (App.svelte) — must match exactly:
1) Initial state (L99108)
- deviceList = [{ name: "--", id: "--", ip: myip, ws: 0, status: false }]
- myip = device from which web loaded (e.g. 127.0.0.1 in dev)
2) onMount (L188200)
- firstDevListRequest = true
- connectToAllDevices() // "сначала подключимся к известному нам ip этого устройства"
3) connectToAllDevices (L226236)
- For each deviceList[i]: deviceList[i].ws = i
- For each with status === false || undefined: wsConnect(i), wsEventAdd(i)
- So initially only ws=0 connects (one WebSocket to myip:81)
4) wsConnect(ws) (L269277)
- getIP(ws) from deviceList (device.ws === ws → device.ip)
- new WebSocket("ws://" + ip + ":81")
5) On socket open (L293308)
- markDeviceStatus(ws, true)
- If firstDevListRequest && ws === 0: wsSendMsg(ws, "/devlist|") // только с первого сокета
- Then send currentPageName (e.g. /| or /list|) to selected or all
6) We respond to /devlist| with blob header "devlis", payload = JSON array of devices.
- Frontend parseBlob (L347354): header = blob.slice(0,6).text(), size = blob.slice(7,11).text()
- getPayloadAsJson(blob, size, out) (L578589): partBlob = blob.slice(size, blob.length) — size coerced to number (payload start offset 12)
- So frame = header(6) + "|0012|"(6) + payload; frontend expects payload from byte 12.
7) On "devlis" (L429436): incDeviceList = out.json, await initDevList()
8) initDevList (L695710)
- If firstDevListRequest: devListOverride() else devListCombine()
- firstDevListRequest = false
- onParced(), selectedDeviceDataRefresh(), connectToAllDevices()
9) devListOverride (L714719): deviceList = incDeviceList; sortList(deviceList); deviceList[0].status = true
- First list entry must be "this device" (same IP). sortList keeps first element first, sorts rest by name.
10) Second connectToAllDevices(): now deviceList has N entries; [0].status already true, so only indices 1..N-1 get wsConnect → we open more sockets (e.g. second device same IP → second connection to mock).
11) saveList (L898905): devListForSave = copy of deviceList, all .status = false, wsSendMsg(selectedWs, "/tsil|" + JSON.stringify(devListForSave))
We store; when udps=0, next /devlist| returns saved list.
"""
import argparse
import asyncio
import json
import math
import socket
import threading
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import Optional
try:
import websockets
except ImportError:
raise SystemExit("Install: pip install websockets")
# Payload starts at byte 12 (header|0012| = 12 bytes)
WS_HEADER_PREFIX = "|0012|"
WS_PREFIX_BYTES = 12
def make_binary_frame(header: str, payload: str) -> bytes:
"""Build binary frame: header(6 chars)|0012| + payload (UTF-8). Backend: sendStringToWs sends same as binary."""
if len(header) != 6:
header = (header + " ")[:6]
prefix = (header + WS_HEADER_PREFIX).encode("utf-8")
body = payload.encode("utf-8")
return prefix + body
def get_layout(device_slot: int) -> list:
"""Layout: 4 pages by meaning (Sensors, Charts, Relays/Light, Params). All 5 widget types."""
prefix = f"/mock/d{device_slot}"
return [
# --- 1. Сенсоры: read-only anydata ---
{"ws": device_slot, "topic": f"{prefix}/temp", "page": "Сенсоры", "widget": "anydata", "descr": "Temperature", "status": "22.5", "after": "°C"},
{"ws": device_slot, "topic": f"{prefix}/hum", "page": "Сенсоры", "widget": "anydata", "descr": "Humidity", "status": "45", "after": "%"},
{"ws": device_slot, "topic": f"{prefix}/pressure", "page": "Сенсоры", "widget": "anydata", "descr": "Pressure", "status": "1013", "after": " hPa"},
{"ws": device_slot, "topic": f"{prefix}/voltage", "page": "Сенсоры", "widget": "anydata", "descr": "Voltage", "status": "3.28", "after": " V"},
{"ws": device_slot, "topic": f"{prefix}/power", "page": "Сенсоры", "widget": "anydata", "descr": "Power", "status": "120", "after": " W", "color": [{"level": 0, "value": ""}, {"level": 200, "value": "#009933"}, {"level": 2000, "value": "#FF9900"}]},
{"ws": device_slot, "topic": f"{prefix}/rssi", "page": "Сенсоры", "widget": "anydata", "descr": "WiFi RSSI", "status": "-65", "after": " dBm"},
# --- 2. Графики 1 и 2 (two cards) ---
{"ws": device_slot, "topic": f"{prefix}/log", "page": "Графики 1", "widget": "chart", "descr": "Temperature log", "status": [], "maxCount": 100},
{"ws": device_slot, "topic": f"{prefix}/log2", "page": "Графики 1", "widget": "chart", "descr": "Humidity log", "status": [], "maxCount": 100},
{"ws": device_slot, "topic": f"{prefix}/log3", "page": "Графики 2", "widget": "chart", "descr": "Power log", "status": [], "maxCount": 100},
{"ws": device_slot, "topic": f"{prefix}/logBar", "page": "Графики 2", "widget": "chart", "type": "bar", "descr": "Bar chart", "status": [], "maxCount": 100},
# --- 3. Реле и свет: toggles + dimmer ---
{"ws": device_slot, "topic": f"{prefix}/relay1", "page": "Реле и свет", "widget": "toggle", "descr": "Relay 1", "status": "0", "sent": False},
{"ws": device_slot, "topic": f"{prefix}/relay2", "page": "Реле и свет", "widget": "toggle", "descr": "Relay 2", "status": "1", "sent": False},
{"ws": device_slot, "topic": f"{prefix}/relay3", "page": "Реле и свет", "widget": "toggle", "descr": "Light", "status": "0", "sent": False},
{"ws": device_slot, "topic": f"{prefix}/fan", "page": "Реле и свет", "widget": "toggle", "descr": "Fan", "status": "0", "sent": False},
{"ws": device_slot, "topic": f"{prefix}/dimmer", "page": "Реле и свет", "widget": "range", "descr": "Dimmer", "status": "50", "min": 0, "max": 100, "after": "%", "sent": False},
# --- 4. Параметры: range + inputs ---
{"ws": device_slot, "topic": f"{prefix}/volume", "page": "Параметры", "widget": "range", "descr": "Volume", "status": "70", "min": 0, "max": 100, "after": "%", "sent": False},
{"ws": device_slot, "topic": f"{prefix}/setpoint", "page": "Параметры", "widget": "range", "descr": "Setpoint", "status": "21", "min": 15, "max": 30, "after": "°C", "sent": False},
{"ws": device_slot, "topic": f"{prefix}/settemp", "page": "Параметры", "widget": "input", "descr": "Set temp", "status": "20.5", "type": "number", "sent": False},
{"ws": device_slot, "topic": f"{prefix}/label", "page": "Параметры", "widget": "input", "descr": "Label", "status": "Room 1", "type": "text", "sent": False},
{"ws": device_slot, "topic": f"{prefix}/alarmtime", "page": "Параметры", "widget": "input", "descr": "Alarm time", "status": "08:30", "type": "time", "sent": False},
{"ws": device_slot, "topic": f"{prefix}/eventdate", "page": "Параметры", "widget": "input", "descr": "Event date", "status": "2025-02-06", "type": "date", "sent": False},
]
# Per-slot state for controls; keys = last segment of topic (id). Merged into get_params.
_params_state: dict = {} # slot -> { topic_id: value }
def _get_params_state(slot: int) -> dict:
if slot not in _params_state:
prefix = f"/mock/d{slot}"
_params_state[slot] = {
"temp": "22.5", "hum": "45", "pressure": "1013", "voltage": "3.28", "power": "120", "rssi": "-65",
"relay1": "0", "relay2": "1", "relay3": "0", "fan": "0",
"dimmer": "50", "volume": "70", "setpoint": "21",
"settemp": "20.5", "label": "Room 1", "alarmtime": "08:30", "eventdate": "2025-02-06",
"log": "", "log2": "", "log3": "", "logBar": "",
}
return _params_state[slot]
def get_params(device_slot: int) -> dict:
"""Params JSON: topic id -> value (for layout widgets)."""
return dict(_get_params_state(device_slot))
def get_chart_data(topic: str, points: int = 20, base_val: float = 20.0, amplitude: float = 5.0) -> dict:
"""Generate chart points: status array of {x: unix_sec, y1: number}."""
import time
now = int(time.time())
step = 3600 # 1 hour
status = []
for i in range(points, 0, -1):
t = now - i * step
val = base_val + amplitude * math.sin(i * 0.5) + (points - i) * 0.1
status.append({"x": t, "y1": round(val, 2)})
return {"topic": topic, "maxCount": 100, "status": status}
# Stored settings per slot (for save/load on System and Connection pages)
_settings_store: dict = {} # slot -> dict
def get_settings(http_host: str, http_port: int, slot: int = 0) -> dict:
"""Settings JSON; serverip must point to mock HTTP for ver.json. System page needs timezone, wg, log, mqttin, i2c, pinSCL, pinSDA, i2cFreq."""
base = {
"name": f"MockDevice-{slot}",
"apssid": "IoTmanager",
"appass": "",
"routerssid": "",
"routerpass": "",
"timezone": 2,
"ntp": "pool.ntp.org",
"weblogin": "admin",
"webpass": "admin",
"mqttServer": "",
"mqttPort": 1883,
"mqttPrefix": "/mock",
"mqttUser": "",
"mqttPass": "",
"serverip": f"http://{http_host}:{http_port}",
"serverlocal": f"http://{http_host}:{http_port}",
"log": False,
"udps": 1,
"settings_": "",
"wg": "group1",
"mqttin": False,
"i2c": False,
"pinSCL": 0,
"pinSDA": 0,
"i2cFreq": 100000,
}
if slot in _settings_store:
base.update(_settings_store[slot])
return base
def get_errors(slot: int = 0) -> dict:
"""Errors JSON for System page (per device): bn, bt, bver, wver, timenow, upt, uptm, uptw, rssi, heap, freeBytes, fl, rst."""
import time
ts = int(time.time())
# Different system info per device slot so UI shows which device is selected
return {
"bn": f"esp32-d{slot}",
"bt": f"2024-0{1 + slot}-0{1 + slot} 12:00",
"bver": f"1.0.{slot}",
"wver": f"4.2.{slot}",
"timenow": str(ts),
"upt": f"{1 + slot}d 0{2 + slot}:{30 + slot * 5}",
"uptm": f"{1 + slot}d 0{2 + slot}:{30 + slot * 5}",
"uptw": f"{1 + slot}d 0{2 + slot}:{30 + slot * 5}",
"rssi": -65 + slot * 10,
"heap": str(120000 - slot * 10000),
"freeBytes": f"{2 - slot * 0.2:.1f}M",
"fl": str(1024 + slot * 512),
"rst": "Software reset" if slot == 0 else "Power-on reset",
}
def get_ssid_list() -> dict:
"""SSID list for connection page."""
return {"0": "MockNetwork", "1": "OtherNetwork"}
# Backend getThisDevice() fields: wg, ip, id, name, status (bool), fv. Frontend table: ws+1, name, ip, id, fv, status, ping (ws assigned by frontend).
def _normalize_device_entry(entry: dict, default_ip: str = "127.0.0.1") -> dict:
"""Ensure device entry has all fields required by List table and backend contract."""
if not isinstance(entry, dict):
return {"name": "--", "id": "--", "ip": default_ip, "status": False, "fv": "-", "wg": ""}
return {
"name": entry.get("name") if entry.get("name") is not None else "--",
"id": entry.get("id") if entry.get("id") is not None else "--",
"ip": entry.get("ip") if entry.get("ip") is not None else default_ip,
"status": bool(entry.get("status")) if entry.get("status") is not None else False,
"fv": str(entry["fv"]) if entry.get("fv") is not None else "-",
"wg": entry.get("wg", ""),
}
def _normalize_devlist(arr: list, default_ip: str = "127.0.0.1") -> list:
"""Return array of normalized device entries; frontend assigns ws by index."""
if not isinstance(arr, list) or len(arr) == 0:
return []
return [_normalize_device_entry(e, default_ip) for e in arr]
def get_devlist(host: str) -> list:
"""Default device list from 'heap' (udps!=0). First entry = this device (IP we're connected to); frontend sets deviceList[0].status=true. Rest = other devices; frontend connects to each."""
return _normalize_devlist([
{"name": "Mock Device 1", "ip": host, "id": "mock-dev-1", "status": False, "fv": "1.0.0", "wg": "group1"},
{"name": "Mock Device 2", "ip": host, "id": "mock-dev-2", "status": False, "fv": "1.0.0", "wg": "group1"},
], host)
# Saved device list from /tsil| (reversed "list"). When udps=0 backend returns from "file", else from "heap" (default).
_saved_devlist: Optional[list] = None
def get_items_json() -> list:
"""Minimal items list."""
return [
{"name": "Выберите элемент", "num": 0},
{"header": "virtual_elments"},
{"global": 0, "name": "Temp", "type": "Reading", "subtype": "AnalogAdc", "id": "temp", "widget": "anydataTmp", "page": "Сенсоры", "descr": "Temperature", "num": 1},
{"global": 0, "name": "Graph", "type": "Writing", "subtype": "Loging", "id": "log", "widget": "chart2", "page": "Графики 1", "descr": "Temperature", "num": 2, "points": 100},
]
def get_widgets_json() -> list:
"""All frontend widget types: anydata, chart (line + bar), toggle, range, input (number, text, time, date)."""
return [
{"name": "anydataDef", "label": "Text", "widget": "anydata", "icon": ""},
{"name": "anydataTmp", "label": "Temperature", "widget": "anydata", "after": "°C", "icon": "speedometer"},
{"name": "chart2", "label": "Chart", "widget": "chart", "icon": ""},
{"name": "chartBar", "label": "Chart bar", "widget": "chart", "type": "bar", "icon": ""},
{"name": "toggleDef", "label": "Toggle", "widget": "toggle", "icon": ""},
{"name": "rangeDef", "label": "Range", "widget": "range", "min": 0, "max": 100, "icon": ""},
{"name": "inputNumber", "label": "Number", "widget": "input", "type": "number", "icon": ""},
{"name": "inputText", "label": "Text", "widget": "input", "type": "text", "icon": ""},
{"name": "inputTime", "label": "Time", "widget": "input", "type": "time", "icon": ""},
{"name": "inputDate", "label": "Date", "widget": "input", "type": "date", "icon": ""},
]
def get_config_json() -> list:
"""Config (array of module configs)."""
return []
def get_ota_json() -> dict:
"""OTA versions placeholder."""
return {}
def get_profile_json() -> dict:
"""Flash profile placeholder."""
return {"board": "esp32", "version": "1.0.0"}
# Global: HTTP host/port for settings.serverip (set at startup)
HTTP_HOST = "127.0.0.1"
HTTP_PORT = 8080
# Slot counter for WebSocket connections
_next_slot = 0
_slot_lock = threading.Lock()
# Connected clients (ws, slot) for periodic broadcast. Backend: publishStatusWs, periodicWsSend, params on change.
_connections: list = [] # list of (websocket, slot)
def assign_slot() -> int:
global _next_slot
with _slot_lock:
s = _next_slot % 2
_next_slot += 1
return s
async def handle_ws_message(ws, message: str, slot: int) -> None:
"""Handle text command from frontend and send responses."""
global _saved_devlist
if "|" not in message:
return
cmd = message.split("|")[0] + "|"
if cmd == "/tst|":
await ws.send("/tstr|")
return
if cmd == "/pi|":
await ws.send("/po|")
return
# Binary responses
def send_bin(header: str, payload: str) -> asyncio.Future:
return ws.send(make_binary_frame(header, payload))
if cmd == "/|":
layout = get_layout(slot)
await send_bin("layout", json.dumps(layout))
# Send params immediately so front has data (front also requests /params| after layout; both trigger updateAllStatuses)
await send_bin("params", json.dumps(get_params(slot)))
return
if cmd == "/params|":
params = get_params(slot)
await send_bin("params", json.dumps(params))
return
if cmd == "/charts|":
layout = get_layout(slot)
for w in layout:
if w.get("widget") != "chart" or not w.get("topic"):
continue
t = w["topic"]
if "log2" in t:
base_val, amp = 45.0, 15.0
elif "log3" in t:
base_val, amp = 100.0, 50.0
else:
base_val, amp = 20.0, 5.0
chart = get_chart_data(w["topic"], base_val=base_val, amplitude=amp)
await send_bin("chartb", json.dumps(chart))
return
if cmd == "/config|":
await send_bin("itemsj", json.dumps(get_items_json()))
await send_bin("widget", json.dumps(get_widgets_json()))
await send_bin("config", json.dumps(get_config_json()))
await send_bin("scenar", "// mock scenario\n")
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
return
if cmd == "/connection|":
await send_bin("widget", json.dumps(get_widgets_json()))
await send_bin("config", json.dumps(get_config_json()))
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
await send_bin("ssidli", json.dumps(get_ssid_list()))
await send_bin("errors", json.dumps(get_errors(slot)))
return
if cmd == "/list|":
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
return
if cmd == "/devlist|":
host = HTTP_HOST
settings = get_settings(HTTP_HOST, HTTP_PORT, slot)
udps = settings.get("udps", 1)
# Backend: udps!=0 -> list from heap (UDP auto); udps=0 -> list from file (saved). Always normalize for table.
if udps == 0 and _saved_devlist and len(_saved_devlist) > 0:
list_to_send = _normalize_devlist(_saved_devlist, host)
else:
list_to_send = get_devlist(host)
await send_bin("devlis", json.dumps(list_to_send))
return
if cmd == "/system|":
await send_bin("errors", json.dumps(get_errors(slot)))
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
return
if cmd == "/dev|":
await send_bin("errors", json.dumps(get_errors(slot)))
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
await send_bin("config", json.dumps(get_config_json()))
await send_bin("itemsj", json.dumps(get_items_json()))
return
if cmd == "/profile|":
await send_bin("otaupd", json.dumps(get_ota_json()))
await send_bin("prfile", json.dumps(get_profile_json()))
return
# /control|key/value - frontend sends id (e.g. relay1) and value; update state and echo status
if cmd == "/control|":
payload = message[len(cmd):].strip()
if "/" in payload:
key, value = payload.split("/", 1)
state = _get_params_state(slot)
state[key] = value
topic = f"/mock/d{slot}/{key}"
await send_bin("status", json.dumps({"topic": topic, "status": value}))
return
# /sgnittes| + JSON - save system settings
if cmd == "/sgnittes|":
payload = message[len(cmd):].strip()
if payload:
try:
data = json.loads(payload)
_settings_store[slot] = data
await send_bin("errors", json.dumps(get_errors(slot)))
except json.JSONDecodeError:
pass
return
# /tsil| + JSON array — save device list (reversed "list"). Backend writes to devlist.json; next /devlist| when udps=0 returns this.
if cmd == "/tsil|":
payload = message[len(cmd) :].strip()
if payload:
try:
data = json.loads(payload)
if isinstance(data, list) and len(data) > 0:
_saved_devlist = _normalize_devlist(data, HTTP_HOST)
print(f"[mock] devlist saved, {len(_saved_devlist)} device(s)")
except (json.JSONDecodeError, TypeError):
pass
return
if cmd == "/clean|":
# Clear logs (no-op; could clear chart data in real device)
return
if cmd == "/reboot|":
# Reboot (no-op in mock)
return
# Save commands: no-op
if cmd in ("/gifnoc|", "/tuoyal|", "/oiranecs|"):
return
async def _send_bin(ws, header: str, payload: str) -> None:
"""Send binary frame to one client (same format as backend sendStringToWs). Must send bytes for client to get Blob."""
frame = make_binary_frame(header, payload)
assert isinstance(frame, bytes), "frame must be bytes so browser receives Blob"
await ws.send(frame)
def _tick_sensor_values() -> None:
"""Update read-only sensor values in _params_state (like backend sensors in loop)."""
t = time.time()
for slot in list(_params_state.keys()):
s = _params_state[slot]
# Small variation around base values
s["temp"] = f"{22 + 3 * math.sin(t * 0.5):.1f}"
s["hum"] = str(int(45 + 10 * math.sin(t * 0.3)))
s["pressure"] = str(1012 + int(5 * math.sin(t * 0.2)))
s["voltage"] = f"{3.2 + 0.1 * math.sin(t):.2f}"
s["power"] = str(int(120 + 30 * math.sin(t * 0.4)))
s["rssi"] = str(int(-65 + 5 * math.sin(t * 0.5)))
# Interval (seconds): backend sends status on sensor change (publishStatusWs); we emulate same.
PARAMS_INTERVAL = 3
# Param keys that are widget values (not chart). Front updateWidget(topic, status) matches layout by full topic.
_PARAM_IDS = [
"temp", "hum", "pressure", "voltage", "power", "rssi",
"relay1", "relay2", "relay3", "fan", "dimmer", "volume", "setpoint",
"settemp", "label", "alarmtime", "eventdate",
]
async def _periodic_broadcast() -> None:
"""Backend: widget updates via publishStatusWs(topic, data) -> sendStringToWs('status', json). Front expects
binary blob header 'status', payload JSON { topic, status }; updateWidget() does layoutJson[i] = jsonConcat(...).
We send one 'status' blob per param so UI reacts. Then chartb for charts."""
await asyncio.sleep(1) # first tick soon after connect
while True:
await asyncio.sleep(PARAMS_INTERVAL)
_tick_sensor_values()
n_conn = len(_connections)
if n_conn > 0:
print(f"[WS] broadcast: {n_conn} client(s), status+chartb", flush=True)
for (ws, slot) in list(_connections):
try:
# websockets 12+ has .state (State.OPEN), not .open; skip check, send() raises if closed
prefix = f"/mock/d{slot}"
state = _get_params_state(slot)
# Send one "status" blob per param (like backend publishStatusWs) so front updateWidget() runs and triggers reactivity
for key in _PARAM_IDS:
if key not in state:
continue
topic = f"{prefix}/{key}"
payload = json.dumps({"topic": topic, "status": state[key]})
await _send_bin(ws, "status", payload)
except Exception:
pass
# Chart points: backend Loging::publishValue sends chartb with one point; front apdateWidgetByArray merges into layout
now = int(time.time())
for (ws, slot) in list(_connections):
try:
prefix = f"/mock/d{slot}"
for topic_suf, base, amp in [("log", 20.0, 5.0), ("log2", 45.0, 15.0), ("log3", 100.0, 50.0), ("logBar", 30.0, 10.0)]:
topic = f"{prefix}/{topic_suf}"
val = base + amp * math.sin(now * 0.1)
point = {"topic": topic, "maxCount": 100, "status": [{"x": now, "y1": round(val, 2)}]}
await _send_bin(ws, "chartb", json.dumps(point))
except Exception:
pass
async def ws_handler(websocket):
slot = assign_slot()
peer = websocket.remote_address
_connections.append((websocket, slot))
print(f"[WS] Client connected {peer} -> slot {slot}")
try:
async for message in websocket:
if isinstance(message, str):
await handle_ws_message(websocket, message, slot)
# Binary from client not expected for these commands
except Exception as e:
print(f"[WS] Error slot {slot}: {e}")
finally:
_connections[:] = [(w, s) for w, s in _connections if w != websocket]
print(f"[WS] Client disconnected slot {slot}")
def run_http_server(host: str, port: int) -> None:
"""Run HTTP server in thread for /iotm/ver.json with CORS."""
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/iotm/ver.json" or self.path == "/iotm/ver.json/":
body = json.dumps({
"esp32": {"versions": ["1.0.0", "1.0.1"]},
"esp8266": {"versions": ["1.0.0"]},
}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
print(f"[HTTP] {args[0]}")
server = HTTPServer((host, port), Handler)
print(f"[HTTP] Serving on http://{host}:{port}")
server.serve_forever()
def get_local_ip() -> str:
"""Prefer 127.0.0.1 for local dev."""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(0)
s.connect(("10.255.255.255", 1))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return "127.0.0.1"
async def main_async(ws_host: str, ws_port: int, http_host: str, http_port: int):
global HTTP_HOST, HTTP_PORT
# For settings.serverip and devlist use 127.0.0.1 when binding 0.0.0.0 (browser on same machine)
HTTP_HOST = "127.0.0.1" if http_host == "0.0.0.0" else http_host
HTTP_PORT = http_port
# Start HTTP server in daemon thread
t = threading.Thread(target=run_http_server, args=(http_host, http_port), daemon=True)
t.start()
local_ip = get_local_ip()
print("")
print("Mock backend: WS ws://{}:{}, HTTP http://{}:{}".format(ws_host, ws_port, http_host, http_port))
print("Add devices with IP {} (or 127.0.0.1 for local).".format(local_ip))
print("")
async with websockets.serve(ws_handler, ws_host, ws_port, ping_interval=20, ping_timeout=20):
asyncio.create_task(_periodic_broadcast())
await asyncio.Future()
def main():
parser = argparse.ArgumentParser(description="IoTManager mock backend (WS + HTTP)")
parser.add_argument("--host", default="0.0.0.0", help="Bind host for WS and HTTP")
parser.add_argument("--ws-port", type=int, default=81, help="WebSocket port")
parser.add_argument("--http-port", type=int, default=8081, help="HTTP port for /iotm/ver.json (avoid 8080 if frontend sirv uses it)")
args = parser.parse_args()
asyncio.run(main_async(args.host, args.ws_port, args.host, args.http_port))
if __name__ == "__main__":
main()