Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 63 additions & 19 deletions src/handlers/vehicle_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING, Final
from typing import TYPE_CHECKING, Any, Final

from saic_ismart_client_ng.exceptions import SaicApiException, SaicLogoutException

Expand Down Expand Up @@ -57,13 +57,51 @@ def __init__(
def publisher(self) -> Publisher:
return self.vehicle_state.publisher

def __report_command_failure(
self,
*,
command: str,
result_topic: str,
detail: str,
exc: Exception | None = None,
) -> None:
if exc is not None:
LOG.exception("Command %s failed: %s", command, detail, exc_info=exc)
else:
LOG.error("Command %s failed: %s", command, detail)
try:
self.publisher.publish_str(result_topic, f"Failed: {detail}")
except Exception:
LOG.warning(
"Failed to publish failure result for command %s",
command,
exc_info=True,
)
try:
error_topic = self.vehicle_state.get_topic(mqtt_topics.COMMAND_ERROR)
event_payload: dict[str, Any] = {
"event_type": "command_error",
"command": command,
"detail": detail,
}
self.publisher.publish_json(error_topic, event_payload)
except Exception:
LOG.warning(
"Failed to publish command error event for command %s",
command,
exc_info=True,
)

async def handle_mqtt_command(self, *, topic: str, payload: str) -> None:
analyzed_topic = self.__get_command_topics(topic)
handler = self.__command_handlers.get(analyzed_topic.command_no_vin)
if not handler:
msg = f"No handler found for command topic {analyzed_topic.command_no_vin}"
self.publisher.publish_str(analyzed_topic.response_no_global, msg)
LOG.error(msg)
self.__report_command_failure(
command=analyzed_topic.command_no_vin,
result_topic=analyzed_topic.response_no_global,
detail=msg,
)
else:
await self.__execute_mqtt_command_handler(
handler=handler, payload=payload, analyzed_topic=analyzed_topic
Expand All @@ -90,19 +128,22 @@ async def __execute_mqtt_command_handler(
if execution_result.clear_command:
self.publisher.clear_topic(topic_no_global)
except MqttGatewayException as e:
self.publisher.publish_str(result_topic, f"Failed: {e.message}")
LOG.exception(e.message, exc_info=e)
self.__report_command_failure(
command=topic, result_topic=result_topic, detail=e.message, exc=e
)
except SaicLogoutException:
LOG.warning(
"API Client was logged out, attempting immediate relogin and retry"
)
try:
await self.relogin_handler.force_login()
except Exception as login_err:
self.publisher.publish_str(
result_topic, f"Failed: relogin failed ({login_err})"
self.__report_command_failure(
command=topic,
result_topic=result_topic,
detail=f"relogin failed ({login_err})",
exc=login_err,
)
LOG.error("Immediate relogin failed", exc_info=login_err)
return
try:
execution_result = await handler.handle(payload)
Expand All @@ -115,19 +156,22 @@ async def __execute_mqtt_command_handler(
if execution_result.clear_command:
self.publisher.clear_topic(topic_no_global)
except Exception as retry_err:
self.publisher.publish_str(
result_topic, f"Failed: {retry_err}"
)
LOG.error(
"Command retry after relogin failed", exc_info=retry_err
self.__report_command_failure(
command=topic,
result_topic=result_topic,
detail=str(retry_err),
exc=retry_err,
)
except SaicApiException as se:
self.publisher.publish_str(result_topic, f"Failed: {se.message}")
LOG.exception(se.message, exc_info=se)
except Exception as se:
self.publisher.publish_str(result_topic, "Failed unexpectedly")
LOG.exception(
"handle_mqtt_command failed with an unexpected exception", exc_info=se
self.__report_command_failure(
command=topic, result_topic=result_topic, detail=se.message, exc=se
)
except Exception as e:
self.__report_command_failure(
command=topic,
result_topic=result_topic,
detail="unexpected error",
exc=e,
)

def __get_command_topics(self, topic: str) -> _MqttCommandTopic:
Expand Down
28 changes: 28 additions & 0 deletions src/integrations/home_assistant/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,34 @@ def _publish_lock(
"lock", name, payload, custom_availability
)

def _publish_event(
self,
topic: str,
name: str,
event_types: list[str],
*,
enabled: bool = True,
entity_category: str | None = None,
device_class: str | None = None,
icon: str | None = None,
custom_availability: HaCustomAvailabilityConfig | None = None,
) -> str:
payload: dict[str, Any] = {
"state_topic": self._get_state_topic(topic),
"event_types": event_types,
"enabled_by_default": enabled,
}
if entity_category is not None:
payload["entity_category"] = entity_category
if device_class is not None:
payload["device_class"] = device_class
if icon is not None:
payload["icon"] = icon

return self._publish_ha_discovery_message(
"event", name, payload, custom_availability
)

def _publish_sensor(
self,
topic: str,
Expand Down
9 changes: 9 additions & 0 deletions src/integrations/home_assistant/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,15 @@ def __publish_ha_discovery_messages_real(self) -> None:
)
self.__publish_lights_sensors()

# Command error event
self._publish_event(
mqtt_topics.COMMAND_ERROR,
"Command error",
["command_error"],
entity_category="diagnostic",
icon="mdi:alert-circle",
)

LOG.debug("Completed publishing Home Assistant discovery messages")

def __publish_drivetrain_charging_sensors(self) -> None:
Expand Down
3 changes: 3 additions & 0 deletions src/mqtt_topics.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,7 @@
TYRES_REAR_LEFT_PRESSURE = TYRES + "/rearLeftPressure"
TYRES_REAR_RIGHT_PRESSURE = TYRES + "/rearRightPressure"

COMMAND = "command"
COMMAND_ERROR = COMMAND + "/error"

VEHICLES = "vehicles"
Loading
Loading