Skip to content

Commit f41fa80

Browse files
nanomadclaude
andcommitted
feat: add Home Assistant Event entity for command errors
Closes #272. When a command to the vehicle fails, a JSON event is now published to the command/error topic so Home Assistant users can build automations around command failures. Changes: - Add _publish_event() to HA discovery base for MQTT Event entities - Register a "Command error" diagnostic event entity in discovery - Publish error events from all failure paths in VehicleCommandHandler - Wrap error event publishing in try-except to prevent masking original errors - Cover the "no handler found" path with error events too Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8edf815 commit f41fa80

4 files changed

Lines changed: 67 additions & 3 deletions

File tree

src/handlers/vehicle_command.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from dataclasses import dataclass
44
import logging
5-
from typing import TYPE_CHECKING, Final
5+
from typing import TYPE_CHECKING, Any, Final
66

77
from saic_ismart_client_ng.exceptions import SaicApiException, SaicLogoutException
88

@@ -57,13 +57,30 @@ def __init__(
5757
def publisher(self) -> Publisher:
5858
return self.vehicle_state.publisher
5959

60+
def __publish_command_error(self, command: str, detail: str) -> None:
61+
try:
62+
error_topic = self.vehicle_state.get_topic(mqtt_topics.COMMAND_ERROR)
63+
event_payload: dict[str, Any] = {
64+
"event_type": "command_error",
65+
"command": command,
66+
"detail": detail,
67+
}
68+
self.publisher.publish_json(error_topic, event_payload)
69+
except Exception:
70+
LOG.warning(
71+
"Failed to publish command error event for command %s",
72+
command,
73+
exc_info=True,
74+
)
75+
6076
async def handle_mqtt_command(self, *, topic: str, payload: str) -> None:
6177
analyzed_topic = self.__get_command_topics(topic)
6278
handler = self.__command_handlers.get(analyzed_topic.command_no_vin)
6379
if not handler:
6480
msg = f"No handler found for command topic {analyzed_topic.command_no_vin}"
6581
self.publisher.publish_str(analyzed_topic.response_no_global, msg)
6682
LOG.error(msg)
83+
self.__publish_command_error(analyzed_topic.command_no_vin, msg)
6784
else:
6885
await self.__execute_mqtt_command_handler(
6986
handler=handler, payload=payload, analyzed_topic=analyzed_topic
@@ -92,17 +109,20 @@ async def __execute_mqtt_command_handler(
92109
except MqttGatewayException as e:
93110
self.publisher.publish_str(result_topic, f"Failed: {e.message}")
94111
LOG.exception(e.message, exc_info=e)
112+
self.__publish_command_error(topic, e.message)
95113
except SaicLogoutException:
96114
LOG.warning(
97115
"API Client was logged out, attempting immediate relogin and retry"
98116
)
99117
try:
100118
await self.relogin_handler.force_login()
101119
except Exception as login_err:
120+
detail = f"relogin failed ({login_err})"
102121
self.publisher.publish_str(
103-
result_topic, f"Failed: relogin failed ({login_err})"
122+
result_topic, f"Failed: {detail}"
104123
)
105124
LOG.error("Immediate relogin failed", exc_info=login_err)
125+
self.__publish_command_error(topic, detail)
106126
return
107127
try:
108128
execution_result = await handler.handle(payload)
@@ -115,20 +135,24 @@ async def __execute_mqtt_command_handler(
115135
if execution_result.clear_command:
116136
self.publisher.clear_topic(topic_no_global)
117137
except Exception as retry_err:
138+
detail = str(retry_err)
118139
self.publisher.publish_str(
119-
result_topic, f"Failed: {retry_err}"
140+
result_topic, f"Failed: {detail}"
120141
)
121142
LOG.error(
122143
"Command retry after relogin failed", exc_info=retry_err
123144
)
145+
self.__publish_command_error(topic, detail)
124146
except SaicApiException as se:
125147
self.publisher.publish_str(result_topic, f"Failed: {se.message}")
126148
LOG.exception(se.message, exc_info=se)
149+
self.__publish_command_error(topic, se.message)
127150
except Exception as se:
128151
self.publisher.publish_str(result_topic, "Failed unexpectedly")
129152
LOG.exception(
130153
"handle_mqtt_command failed with an unexpected exception", exc_info=se
131154
)
155+
self.__publish_command_error(topic, str(se))
132156

133157
def __get_command_topics(self, topic: str) -> _MqttCommandTopic:
134158
global_topic_removed = topic.removeprefix(self.global_mqtt_topic).removeprefix(

src/integrations/home_assistant/base.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,34 @@ def _publish_lock(
230230
"lock", name, payload, custom_availability
231231
)
232232

233+
def _publish_event(
234+
self,
235+
topic: str,
236+
name: str,
237+
event_types: list[str],
238+
*,
239+
enabled: bool = True,
240+
entity_category: str | None = None,
241+
device_class: str | None = None,
242+
icon: str | None = None,
243+
custom_availability: HaCustomAvailabilityConfig | None = None,
244+
) -> str:
245+
payload: dict[str, Any] = {
246+
"state_topic": self._get_state_topic(topic),
247+
"event_types": event_types,
248+
"enabled_by_default": enabled,
249+
}
250+
if entity_category is not None:
251+
payload["entity_category"] = entity_category
252+
if device_class is not None:
253+
payload["device_class"] = device_class
254+
if icon is not None:
255+
payload["icon"] = icon
256+
257+
return self._publish_ha_discovery_message(
258+
"event", name, payload, custom_availability
259+
)
260+
233261
def _publish_sensor(
234262
self,
235263
topic: str,

src/integrations/home_assistant/discovery.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,15 @@ def __publish_ha_discovery_messages_real(self) -> None:
327327
)
328328
self.__publish_lights_sensors()
329329

330+
# Command error event
331+
self._publish_event(
332+
mqtt_topics.COMMAND_ERROR,
333+
"Command error",
334+
["command_error"],
335+
entity_category="diagnostic",
336+
icon="mdi:alert-circle",
337+
)
338+
330339
LOG.debug("Completed publishing Home Assistant discovery messages")
331340

332341
def __publish_drivetrain_charging_sensors(self) -> None:

src/mqtt_topics.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,7 @@
179179
TYRES_REAR_LEFT_PRESSURE = TYRES + "/rearLeftPressure"
180180
TYRES_REAR_RIGHT_PRESSURE = TYRES + "/rearRightPressure"
181181

182+
COMMAND = "command"
183+
COMMAND_ERROR = COMMAND + "/error"
184+
182185
VEHICLES = "vehicles"

0 commit comments

Comments
 (0)