From 794bac923193791a780b435ee9b1f4d73d449d90 Mon Sep 17 00:00:00 2001 From: Chris Oloff Date: Tue, 18 Nov 2025 12:51:30 +0200 Subject: [PATCH 1/2] reconnect and connection cleanup working --- sample.py | 16 ++++++++++++ watt42_viewlib/__init__.py | 53 +++++++++++++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 sample.py diff --git a/sample.py b/sample.py new file mode 100644 index 0000000..d4f2390 --- /dev/null +++ b/sample.py @@ -0,0 +1,16 @@ +import panel + +from watt42_viewlib import attach_w42_state + +w42_state = panel.rx(None) +attach_w42_state(rx_var=w42_state, system_id="fb2b91ce-383e-4356-96b3-b6405dacb353") + +state_as_text = panel.bind(lambda s: f"State is {s}", w42_state) + +state_pane = panel.pane.Markdown(state_as_text, sizing_mode='stretch_width') + +_ = panel.template.FastListTemplate( + title="Sample W42 View App", + sidebar=[panel.pane.Markdown("This is a sample sidebar.")], + main=[panel.pane.Markdown("Welcome to the main content area!"), state_pane], +).servable() diff --git a/watt42_viewlib/__init__.py b/watt42_viewlib/__init__.py index b93c4f7..6f2d7db 100644 --- a/watt42_viewlib/__init__.py +++ b/watt42_viewlib/__init__.py @@ -1,5 +1,52 @@ +import asyncio +import json +import panel -print(f"hello from viewlib") +from websockets import ConnectionClosed +from websockets.asyncio.client import connect -def greet(name: str) -> str: - return f"Hello, {name}!" +def attach_w42_state(rx_var: panel.rx, system_id: str): + + WS_URL = "ws://localhost:8000/ws/systems" # TODO: make configurable + + must_reconnect = True + + async def subscribe_to_system(): + + print(f"Starting connection to {WS_URL} for system_id {system_id}") + + async for websocket in connect(WS_URL): + try: + print(f"Connected to {WS_URL}") + send_response = await websocket.send(json.dumps({ + "action": "subscribe", + "system_id": system_id + })) + print(f"Subscribed to system {system_id}, waiting for messages..., send_response={send_response}") + async for message in websocket: + as_json = json.loads(message) + rx_var.rx.value = as_json['change']['state'] + except ConnectionClosed: + if must_reconnect: + print("connection closed, retrying...") + continue + else: + print("Not retrying connection.") + return + except Exception as e: + print(f"connection error, will retry: {e}") + await asyncio.sleep(2) + + print(f"Attaching W42 state for system_id {system_id}") + panel.state.execute(subscribe_to_system) + + async def session_destroyed(): + print("Stop retrying connection (session destroyed)") + nonlocal must_reconnect + must_reconnect = False + + def on_session_destroyed(session_id): + print(f"Session {session_id} destroyed") + panel.state.execute(session_destroyed) + + panel.state.on_session_destroyed(on_session_destroyed) From 58c98b01192841eba79cae5669a56b0a0bc3594a Mon Sep 17 00:00:00 2001 From: Chris Oloff Date: Tue, 18 Nov 2025 13:06:15 +0200 Subject: [PATCH 2/2] logger instead of print, prettier sample --- sample.py | 5 +++-- watt42_viewlib/__init__.py | 21 ++++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/sample.py b/sample.py index d4f2390..f3e76e7 100644 --- a/sample.py +++ b/sample.py @@ -1,3 +1,4 @@ +import json import panel from watt42_viewlib import attach_w42_state @@ -5,12 +6,12 @@ from watt42_viewlib import attach_w42_state w42_state = panel.rx(None) attach_w42_state(rx_var=w42_state, system_id="fb2b91ce-383e-4356-96b3-b6405dacb353") -state_as_text = panel.bind(lambda s: f"State is {s}", w42_state) +state_as_text = panel.bind(lambda s: f"W42 State:\n\n```\n{json.dumps(s, indent=2)}\n```\n\nReplace this with some awesome visuals", w42_state) state_pane = panel.pane.Markdown(state_as_text, sizing_mode='stretch_width') _ = panel.template.FastListTemplate( title="Sample W42 View App", sidebar=[panel.pane.Markdown("This is a sample sidebar.")], - main=[panel.pane.Markdown("Welcome to the main content area!"), state_pane], + main=[panel.pane.Markdown("# Welcome to the Main Content Area"), state_pane], ).servable() diff --git a/watt42_viewlib/__init__.py b/watt42_viewlib/__init__.py index 6f2d7db..ac1a9a0 100644 --- a/watt42_viewlib/__init__.py +++ b/watt42_viewlib/__init__.py @@ -1,10 +1,13 @@ import asyncio import json +import logging import panel from websockets import ConnectionClosed from websockets.asyncio.client import connect +logger = logging.getLogger(__name__) + def attach_w42_state(rx_var: panel.rx, system_id: str): WS_URL = "ws://localhost:8000/ws/systems" # TODO: make configurable @@ -13,40 +16,40 @@ def attach_w42_state(rx_var: panel.rx, system_id: str): async def subscribe_to_system(): - print(f"Starting connection to {WS_URL} for system_id {system_id}") + logger.info(f"Starting connection to {WS_URL} for system_id {system_id}") async for websocket in connect(WS_URL): try: - print(f"Connected to {WS_URL}") + logger.info(f"Connected to {WS_URL}") send_response = await websocket.send(json.dumps({ "action": "subscribe", "system_id": system_id })) - print(f"Subscribed to system {system_id}, waiting for messages..., send_response={send_response}") + logger.info(f"Subscribed to system {system_id}, waiting for messages..., send_response={send_response}") async for message in websocket: as_json = json.loads(message) rx_var.rx.value = as_json['change']['state'] except ConnectionClosed: if must_reconnect: - print("connection closed, retrying...") + logger.info("connection closed, retrying...") continue else: - print("Not retrying connection.") + logger.info("Not retrying connection.") return except Exception as e: - print(f"connection error, will retry: {e}") + logger.info(f"connection error, will retry: {e}") await asyncio.sleep(2) - print(f"Attaching W42 state for system_id {system_id}") + logger.info(f"Attaching W42 state for system_id {system_id}") panel.state.execute(subscribe_to_system) async def session_destroyed(): - print("Stop retrying connection (session destroyed)") + logger.info("Stop retrying connection (session destroyed)") nonlocal must_reconnect must_reconnect = False def on_session_destroyed(session_id): - print(f"Session {session_id} destroyed") + logger.info(f"Session {session_id} destroyed") panel.state.execute(session_destroyed) panel.state.on_session_destroyed(on_session_destroyed)