From c566a0086358a14edf5e588978fa317ad7a0c7be Mon Sep 17 00:00:00 2001 From: DmitryBorisenko33 Date: Mon, 9 Feb 2026 00:06:48 +0100 Subject: [PATCH] Add layout and configuration handling in mock_backend; update WebSocketManager and App.svelte for improved state management --- scripts/mock_backend.py | 128 +++++++++++++++++++++++++++++++----- src/App.svelte | 22 +++++-- src/lib/WebSocketManager.js | 10 ++- 3 files changed, 138 insertions(+), 22 deletions(-) diff --git a/scripts/mock_backend.py b/scripts/mock_backend.py index 782b9fc..2387ccf 100644 --- a/scripts/mock_backend.py +++ b/scripts/mock_backend.py @@ -112,6 +112,43 @@ def get_layout(device_slot: int) -> list: # Per-slot state for controls; keys = last segment of topic (id). Merged into get_params. _params_state: dict = {} # slot -> { topic_id: value } +def _topic_id(topic: str) -> str: + """Last segment of topic (e.g. /mock/d0/relay1 -> relay1).""" + if not topic or "/" not in topic: + return topic or "" + return topic.rstrip("/").split("/")[-1] + + +def _ensure_params_for_layout(slot: int, layout: list) -> None: + """Add missing topic ids from layout to _params_state[slot] so added widgets get data.""" + state = _get_params_state(slot) + for w in layout: + topic = w.get("topic") or "" + wid = _topic_id(topic) + if not wid or wid in state: + continue + widget_type = (w.get("widget") or "").lower() + if "chart" in widget_type: + state[wid] = "" + elif "toggle" in widget_type: + state[wid] = "0" + elif "range" in widget_type: + state[wid] = str(w.get("min", 0) if isinstance(w.get("min"), (int, float)) else 50) + elif "input" in widget_type: + itype = w.get("type") or "number" + if itype == "number": + state[wid] = "20" + elif itype == "text": + state[wid] = "Room" + elif itype == "time": + state[wid] = "08:00" + elif itype == "date": + state[wid] = "2025-01-01" + else: + state[wid] = "0" + else: + state[wid] = "0" + def _get_params_state(slot: int) -> dict: if slot not in _params_state: @@ -126,9 +163,12 @@ def _get_params_state(slot: int) -> dict: 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_params(device_slot: int, layout: Optional[list] = None) -> dict: + """Params JSON: topic id -> value (for layout widgets). If layout given, ensure all its topic ids have state.""" + state = _get_params_state(device_slot) + if layout: + _ensure_params_for_layout(device_slot, layout) + return dict(state) def get_chart_data(topic: str, points: int = 20, base_val: float = 20.0, amplitude: float = 5.0) -> dict: @@ -241,15 +281,41 @@ def get_devlist(host: str) -> list: # Saved device list from /tsil| (reversed "list"). When udps=0 backend returns from "file", else from "heap" (default). _saved_devlist: Optional[list] = None +# Configurator: saved layout/config/scenario per slot (from /tuoyal|, /gifnoc|, /oiranecs|) +_saved_layout: dict = {} # slot -> list (layout JSON array) +_saved_config: dict = {} # slot -> list (config JSON array) +_saved_scenario: dict = {} # slot -> str (scenario text) def get_items_json() -> list: - """Minimal items list.""" + """Items list covering all widget types (anydata, chart, toggle, range, input).""" 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}, + # anydata + {"global": 0, "name": "Text", "type": "Reading", "subtype": "Variable", "id": "text", "widget": "anydataDef", "page": "Сенсоры", "descr": "Text", "num": 1}, + {"global": 0, "name": "Temperature", "type": "Reading", "subtype": "AnalogAdc", "id": "temp", "widget": "anydataTmp", "page": "Сенсоры", "descr": "Temperature", "num": 2}, + {"global": 0, "name": "Humidity", "type": "Reading", "subtype": "Variable", "id": "hum", "widget": "anydataDef", "page": "Сенсоры", "descr": "Humidity", "num": 3}, + {"global": 0, "name": "Pressure", "type": "Reading", "subtype": "Variable", "id": "pressure", "widget": "anydataDef", "page": "Сенсоры", "descr": "Pressure", "num": 4}, + {"global": 0, "name": "Voltage", "type": "Reading", "subtype": "Variable", "id": "voltage", "widget": "anydataDef", "page": "Сенсоры", "descr": "Voltage", "num": 5}, + {"global": 0, "name": "Power", "type": "Reading", "subtype": "Variable", "id": "power", "widget": "anydataDef", "page": "Сенсоры", "descr": "Power", "num": 6}, + {"global": 0, "name": "RSSI", "type": "Reading", "subtype": "Variable", "id": "rssi", "widget": "anydataDef", "page": "Сенсоры", "descr": "WiFi RSSI", "num": 7}, + # chart + {"global": 0, "name": "Chart line", "type": "Writing", "subtype": "Loging", "id": "log", "widget": "chart2", "page": "Графики 1", "descr": "Chart", "num": 10, "points": 100}, + {"global": 0, "name": "Chart bar", "type": "Writing", "subtype": "Loging", "id": "logBar", "widget": "chartBar", "page": "Графики 2", "descr": "Bar chart", "num": 11, "points": 100}, + # toggle + {"global": 0, "name": "Toggle", "type": "Writing", "subtype": "ButtonOut", "id": "relay", "widget": "toggleDef", "page": "Реле и свет", "descr": "Relay", "num": 20}, + {"global": 0, "name": "Light", "type": "Writing", "subtype": "ButtonOut", "id": "light", "widget": "toggleDef", "page": "Реле и свет", "descr": "Light", "num": 21}, + {"global": 0, "name": "Fan", "type": "Writing", "subtype": "ButtonOut", "id": "fan", "widget": "toggleDef", "page": "Реле и свет", "descr": "Fan", "num": 22}, + # range + {"global": 0, "name": "Dimmer", "type": "Writing", "subtype": "Variable", "id": "dimmer", "widget": "rangeDef", "page": "Параметры", "descr": "Dimmer", "num": 30}, + {"global": 0, "name": "Volume", "type": "Writing", "subtype": "Variable", "id": "volume", "widget": "rangeDef", "page": "Параметры", "descr": "Volume", "num": 31}, + {"global": 0, "name": "Setpoint", "type": "Writing", "subtype": "Variable", "id": "setpoint", "widget": "rangeDef", "page": "Параметры", "descr": "Setpoint", "num": 32}, + # input + {"global": 0, "name": "Input number", "type": "Reading", "subtype": "Variable", "id": "settemp", "widget": "inputNumber", "page": "Параметры", "descr": "Number", "num": 40}, + {"global": 0, "name": "Input text", "type": "Reading", "subtype": "Variable", "id": "label", "widget": "inputText", "page": "Параметры", "descr": "Label", "num": 41}, + {"global": 0, "name": "Input time", "type": "Reading", "subtype": "Variable", "id": "alarmtime", "widget": "inputTime", "page": "Параметры", "descr": "Time", "num": 42}, + {"global": 0, "name": "Input date", "type": "Reading", "subtype": "Variable", "id": "eventdate", "widget": "inputDate", "page": "Параметры", "descr": "Date", "num": 43}, ] @@ -305,11 +371,12 @@ def assign_slot() -> int: async def handle_ws_message(ws, message: str, slot: int) -> None: """Handle text command from frontend and send responses.""" - global _saved_devlist + global _saved_devlist, _saved_layout, _saved_config, _saved_scenario if "|" not in message: return cmd = message.split("|")[0] + "|" + # Heartbeat/ping: same as original IoTManager WsServer.cpp — reply immediately so frontend can measure RTT (device.ping) if cmd == "/tst|": await ws.send("/tstr|") return @@ -322,19 +389,20 @@ async def handle_ws_message(ws, message: str, slot: int) -> None: return ws.send(make_binary_frame(header, payload)) if cmd == "/|": - layout = get_layout(slot) + layout = _saved_layout.get(slot) if slot in _saved_layout else 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))) + # Send params immediately so front has data (incl. for saved-layout widgets) + await send_bin("params", json.dumps(get_params(slot, layout))) return if cmd == "/params|": - params = get_params(slot) + layout = _saved_layout.get(slot) if slot in _saved_layout else get_layout(slot) + params = get_params(slot, layout) await send_bin("params", json.dumps(params)) return if cmd == "/charts|": - layout = get_layout(slot) + layout = _saved_layout.get(slot) if slot in _saved_layout else get_layout(slot) for w in layout: if w.get("widget") != "chart" or not w.get("topic"): continue @@ -352,8 +420,10 @@ async def handle_ws_message(ws, message: str, slot: int) -> None: 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") + config_data = _saved_config[slot] if slot in _saved_config else get_config_json() + await send_bin("config", json.dumps(config_data)) + scenario_txt = _saved_scenario.get(slot, "// mock scenario\n") + await send_bin("scenar", scenario_txt if isinstance(scenario_txt, str) else "// mock scenario\n") await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot))) return @@ -442,8 +512,34 @@ async def handle_ws_message(ws, message: str, slot: int) -> None: # Reboot (no-op in mock) return - # Save commands: no-op - if cmd in ("/gifnoc|", "/tuoyal|", "/oiranecs|"): + # Save configurator data (layout, config, scenario) per slot + if cmd == "/tuoyal|": + payload = message[len(cmd) :].strip() + if payload: + try: + data = json.loads(payload) + if isinstance(data, list): + _saved_layout[slot] = data + _ensure_params_for_layout(slot, data) + print(f"[mock] layout saved for slot {slot}, {len(data)} widget(s)") + except json.JSONDecodeError: + pass + return + if cmd == "/gifnoc|": + payload = message[len(cmd) :].strip() + if payload: + try: + data = json.loads(payload) + if isinstance(data, list): + _saved_config[slot] = data + print(f"[mock] config saved for slot {slot}, {len(data)} item(s)") + except json.JSONDecodeError: + pass + return + if cmd == "/oiranecs|": + payload = message[len(cmd) :].strip() + _saved_scenario[slot] = payload if payload is not None else "" + print(f"[mock] scenario saved for slot {slot}") return diff --git a/src/App.svelte b/src/App.svelte index 9b3cec8..c48a944 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -60,6 +60,9 @@ let versionsList = {}; let choosingVersion = undefined; let selectedWs = 0; + let socketConnected = false; + let percent = 0; + let remainingTimeout = 60; let opened = true; let preventMove = false; @@ -67,7 +70,6 @@ let showInput = false; $: currentPageName = wsManager.currentPageName; - $: remainingTimeout = wsManager.remainingTimeout; $: wsManager.choosingVersion = choosingVersion; router.subscribe(handleNavigation); @@ -148,6 +150,9 @@ opened = screenSize > 900; wsManager.firstDevListRequest = true; wsManager.selectedDeviceDataRefresh(); + socketConnected = wsManager.socketConnected; + percent = wsManager.percent; + remainingTimeout = wsManager.remainingTimeout; wsManager.connectToAllDevices(); wsManager.startReconnectTask(); @@ -155,9 +160,16 @@ layoutJson = data.layoutJson || []; pages = data.pages || []; if (data.pageReady) pageReady = data.pageReady; + if (data.configJson !== undefined) configJson = data.configJson; + if (data.scenarioTxt !== undefined) scenarioTxt = data.scenarioTxt; }); eventEmitter.on("deviceListUpdated", () => { - deviceList = wsManager.deviceList; + deviceList = [...wsManager.deviceList]; + socketConnected = wsManager.socketConnected; + }); + eventEmitter.on("reconnectTick", (data) => { + percent = data.percent; + remainingTimeout = data.remainingTimeout; }); eventEmitter.on("configUpdated", (data) => { configJson = data.configJson || []; @@ -190,7 +202,7 @@ {deviceList} bind:selectedWs showDropdown={wsManager.currentPageName !== "/|" && wsManager.currentPageName !== "/list|"} - socketConnected={wsManager.socketConnected} + {socketConnected} {devicesDropdownChange} /> onCheck()} userdata={wsManager.userdata} /> @@ -198,7 +210,7 @@