From b8388db18de8c746423e1dd251a60a802bcc41fa Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Fri, 7 Feb 2025 06:08:49 -0500 Subject: [PATCH 1/3] klippy_connection: improve subscription request handling If the connection requesting the subscription already had an outstanding subscription, remove it before processing. This prevents immediate status updates from being pushed to the requesting connection. Don't add connections to the dictionary of subscribed connections if the request is an empty object, as this is a request to unsubscribe from all objects. Signed-off-by: Eric Callahan --- moonraker/components/klippy_connection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/moonraker/components/klippy_connection.py b/moonraker/components/klippy_connection.py index 40030eb47..565cd60ba 100644 --- a/moonraker/components/klippy_connection.py +++ b/moonraker/components/klippy_connection.py @@ -624,6 +624,8 @@ async def _request_subscripton(self, web_request: WebRequest) -> Dict[str, Any]: raise self.server.error( "No connection associated with subscription request" ) + # if the connection has an existing subscription pop it off + self.subscriptions.pop(conn, None) requested_sub: Subscription = args.get('objects', {}) all_subs: Subscription = dict(requested_sub) # Build the subscription request from a superset of all client subscriptions @@ -695,7 +697,8 @@ async def _request_subscripton(self, web_request: WebRequest) -> Dict[str, Any]: if obj_name not in all_status: del self.subscription_cache[obj_name] result['status'] = pruned_status - self.subscriptions[conn] = requested_sub + if requested_sub: + self.subscriptions[conn] = requested_sub return result async def _request_standard( From fb6e416e1ceab9360c93d1bbd5554ffe08a5a2a5 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Fri, 7 Feb 2025 06:15:26 -0500 Subject: [PATCH 2/3] mqtt: push Klipper status updates after subscription This resolves an issue where MQTT clients miss the first status update after a Klipper restart. Signed-off-by: Eric Callahan --- moonraker/components/klippy_apis.py | 3 +++ moonraker/components/mqtt.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/moonraker/components/klippy_apis.py b/moonraker/components/klippy_apis.py index 26c96e3e1..b7a869bc6 100644 --- a/moonraker/components/klippy_apis.py +++ b/moonraker/components/klippy_apis.py @@ -266,12 +266,15 @@ async def subscribe_from_transport( objects: Mapping[str, Optional[List[str]]], transport: APITransport, default: Union[Sentinel, _T] = Sentinel.MISSING, + full_response: bool = False ) -> Union[_T, Dict[str, Any]]: params = {"objects": dict(objects)} result = await self._send_klippy_request( SUBSCRIPTION_ENDPOINT, params, default, transport ) if isinstance(result, dict) and "status" in result: + if full_response: + return result return result["status"] if default is not Sentinel.MISSING: return default diff --git a/moonraker/components/mqtt.py b/moonraker/components/mqtt.py index 48f88545d..06164d94c 100644 --- a/moonraker/components/mqtt.py +++ b/moonraker/components/mqtt.py @@ -453,9 +453,13 @@ async def component_init(self) -> None: async def _handle_klippy_started(self, state: KlippyState) -> None: if self.status_objs: kapi: KlippyAPI = self.server.lookup_component("klippy_apis") - await kapi.subscribe_from_transport( - self.status_objs, self, default=None, + result = await kapi.subscribe_from_transport( + self.status_objs, self, default=None, full_response=True ) + if result is not None: + status: Dict[str, Any] = result["status"] + eventtime: float = result["eventtime"] + self.send_status(status, eventtime) if self.status_update_timer is not None: self.status_update_timer.start(delay=self.status_interval) @@ -496,7 +500,7 @@ def _on_connect(self, {'server': 'online'}, retain=True) subs = [(k, v[0]) for k, v in self.subscribed_topics.items()] if subs: - res, msg_id = client.subscribe(subs) + _, msg_id = client.subscribe(subs) if msg_id is not None: sub_fut: asyncio.Future = self.eventloop.create_future() topics = list(self.subscribed_topics.keys()) @@ -618,7 +622,7 @@ def subscribe_topic(self, need_sub = qos != prev_qos self.subscribed_topics[topic] = (qos, sub_handles) if self.is_connected() and need_sub: - res, msg_id = self.client.subscribe(topic, qos) + _, msg_id = self.client.subscribe(topic, qos) if msg_id is not None: sub_fut: asyncio.Future = self.eventloop.create_future() sub_fut.add_done_callback( @@ -636,7 +640,7 @@ def unsubscribe(self, hdl: SubscriptionHandle) -> None: pass if not sub_hdls: del self.subscribed_topics[topic] - res, msg_id = self.client.unsubscribe(topic) + _, msg_id = self.client.unsubscribe(topic) if msg_id is not None: unsub_fut: asyncio.Future = self.eventloop.create_future() unsub_fut.add_done_callback( @@ -814,7 +818,8 @@ def _publish_status_update(self, status: Dict[str, Any], eventtime: float) -> No payload = {'eventtime': eventtime, 'value': objval[statekey]} self.publish_topic( f"{self.klipper_state_prefix}/{objkey}/{statekey}", - payload, retain=True) + payload, retain=True + ) else: payload = {'eventtime': eventtime, 'status': status} self.publish_topic(self.klipper_status_topic, payload) From 1117890327281ec3142cf260d72357c06bad854e Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Fri, 7 Feb 2025 10:31:56 -0500 Subject: [PATCH 3/3] docs: update changelog Also add a warning to the "webhooks" printer object description stating that it should not be used to receive async "ready" updates. Signed-off-by: Eric Callahan --- docs/changelog.md | 8 ++++++++ docs/printer_objects.md | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 0d7536ffb..a73f8ea6d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -30,9 +30,17 @@ The format is based on [Keep a Changelog]. should use the new endpoint, however it may be desirable to also support the deprecated `full` and `client` endpoints for compatibility with older API versions. +- **build**: Bump PDM-Backend to 2.4.3. +- **build**: Bump Apprise to 1.9.2 +- **build**: Bump Tornado to 6.4.2 +- **build**: Bump Streaming-form-data to 1.19.1 +- **build**: Bump Jinja2 to 3.1.5 ### Fixed - **python_deploy**: fix "dev" channel updates for GitHub sources. +- **mqtt**: Publish the result of the Klipper status subscription request. + This fixes issues with MQTT clients missing the initial status updates + after Klippy restarts. ### Added - **application**: Verify that a filename is present when parsing the diff --git a/docs/printer_objects.md b/docs/printer_objects.md index 147ce6aeb..a795f19ba 100644 --- a/docs/printer_objects.md +++ b/docs/printer_objects.md @@ -39,6 +39,17 @@ The format is [X, Y, Z, E]. ## webhooks +/// warning +Websocket and Unix Socket subscribers to the `webhooks` object +should not rely on it for asynchronous `startup`, `ready`, or +`error` state updates. By the time Moonraker has established +a connection to Klipper it is possible that the `webhooks` +state is already beyond the startup phase. + +MQTT subscriptions will publish the first `state` detected +after Klippy exits the `startup` phase. +/// + ```{.json title="Printer Object Example"} { "state": "startup",