Skip to content

Commit 3e2caad

Browse files
authored
Merge pull request #309 from unsnow-iac/fix/climate-settings-500
Gracefully handle climate-settings HTTP 500 (fixes #294)
2 parents 987544f + cab2b10 commit 3e2caad

2 files changed

Lines changed: 33 additions & 14 deletions

File tree

custom_components/toyota/__init__.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -593,11 +593,21 @@ async def _refresh_one_vehicle(vehicle: Vehicle) -> VehicleData:
593593
# Bounded so a slow/flaky non-status endpoint can't stall first_refresh
594594
# past HA's setup budget. On timeout the TimeoutError propagates to the
595595
# caller's per-vehicle handler, which serves cache or a stub - same
596-
# degrade path as any other transient Toyota failure.
597-
await asyncio.wait_for(
598-
_call_tagged("vehicle.update", vin, vehicle.update(skip=["status"])),
599-
STATUS_FETCH_BUDGET_S,
600-
)
596+
# degrade path as any other transient Toyota failure. A climate-settings
597+
# (or other endpoint) HTTP 500 surfaces here as a ToyotaApiError/
598+
# ToyotaInternalError - swallow it so a single bad endpoint doesn't fail
599+
# the whole refresh; the rest of the snapshot still builds from cache.
600+
try:
601+
await asyncio.wait_for(
602+
_call_tagged("vehicle.update", vin, vehicle.update(skip=["status"])),
603+
STATUS_FETCH_BUDGET_S,
604+
)
605+
except (ToyotaApiError, ToyotaInternalError) as ex:
606+
_LOGGER.warning(
607+
"vehicle.update partial failure for vin=...%s (%s), continuing",
608+
(vin or "")[-6:],
609+
_error_code(ex),
610+
)
601611

602612
# Build snapshot for the strategy.
603613
current_odometer_km: float | None = None

custom_components/toyota/climate.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def __init__(
123123
def _load_climate_settings_from_coordinator(self) -> None:
124124
"""Load climate settings from coordinator data if available."""
125125
try:
126-
if not self.vehicle or not hasattr(self.vehicle, "climate_settings"):
126+
if not self.vehicle or not getattr(self.vehicle, "climate_settings", None):
127127
_LOGGER.debug("Vehicle climate_settings not yet available")
128128
return
129129

@@ -147,18 +147,24 @@ def _load_temperature_settings(self) -> None:
147147
"""Load temperature settings from climate_settings."""
148148
climate_settings = self.vehicle.climate_settings
149149
target_temperature = climate_settings.temperature
150-
if target_temperature is not None:
150+
if target_temperature is not None and target_temperature.value is not None:
151151
self._attr_target_temperature = target_temperature.value
152-
self._attr_min_temp = getattr(climate_settings, "min_temp", 18)
153-
self._attr_max_temp = getattr(climate_settings, "max_temp", 29)
154-
self._attr_target_temperature_step = getattr(
155-
climate_settings, "temp_interval", 1
152+
# `or <default>` guards against the attribute existing but being None
153+
# (climate-settings HTTP 500). A None min/max_temp makes HA core's
154+
# set_temperature validation do `float < None` -> TypeError; keep the
155+
# __init__ defaults (18/29/1) instead.
156+
self._attr_min_temp = getattr(climate_settings, "min_temp", None) or 18
157+
self._attr_max_temp = getattr(climate_settings, "max_temp", None) or 29
158+
self._attr_target_temperature_step = (
159+
getattr(climate_settings, "temp_interval", None) or 1
156160
)
157161

158162
def _load_defrost_settings(self) -> None:
159163
"""Load defrost settings from climate_settings operations."""
160164
climate_settings = self.vehicle.climate_settings
161-
operations = getattr(climate_settings, "operations", [])
165+
# API can return operations=None (e.g. climate-settings HTTP 500);
166+
# `or []` guards against the attribute existing but being None.
167+
operations = getattr(climate_settings, "operations", None) or []
162168
for operation in filter(lambda o: o.category_name == "defrost", operations):
163169
for param in operation.parameters:
164170
if param.name == "frontDefrost":
@@ -224,8 +230,11 @@ def _create_climate_settings(self) -> ClimateSettingsModel:
224230
Returns:
225231
ClimateSettingsModel configured with the specified settings
226232
"""
227-
# Start with existing operations
228-
ac_operations = self.vehicle.climate_settings.operations.copy()
233+
# Start with existing operations. climate_settings itself (not just
234+
# .operations) can be None when the climate-settings endpoint 500'd, so
235+
# getattr through both to avoid an AttributeError on the control path.
236+
climate_settings = getattr(self.vehicle, "climate_settings", None)
237+
ac_operations = (getattr(climate_settings, "operations", None) or []).copy()
229238

230239
# Find and replace the defrost operation
231240
for i, operation in enumerate(ac_operations):

0 commit comments

Comments
 (0)