Skip to content

Gracefully handle climate-settings HTTP 500 (fixes #294)#309

Merged
deejay1 merged 4 commits into
pytoyoda:mainfrom
unsnow-iac:fix/climate-settings-500
Jun 24, 2026
Merged

Gracefully handle climate-settings HTTP 500 (fixes #294)#309
deejay1 merged 4 commits into
pytoyoda:mainfrom
unsnow-iac:fix/climate-settings-500

Conversation

@unsnow-iac

Copy link
Copy Markdown
Contributor

Problem

On some HEV/PHEV vehicles the /v1/global/remote/climate-settings endpoint returns HTTP 500 even though remote climate works in the MyToyota app (#294). Two failure modes result:

  1. The unguarded vehicle.update() call propagates the error and aborts the entire refresh cycle, taking otherwise-healthy endpoints down with it.
  2. When climate_settings does come back partially populated, operations/min_temp/max_temp are None, so filter(operation, None) raises TypeError every update cycle and HA-core's set_temperature validation does float < NoneTypeError. Logs fill with retries/tracebacks and the climate entity never settles.

Fix (integration-layer, minimal)

  • Wrap the manual vehicle.update(skip=["status"]) in try/except (ToyotaApiError, ToyotaInternalError) and log a warning, so one failing endpoint no longer aborts the refresh.
  • None-guard climate_settings in climate.py: fall back to the existing __init__ defaults (18/29/1) for min/max/step and treat operations=None as []. The climate entity degrades gracefully instead of erroring.

This is the auto-disable-style graceful handling #294's reporter explicitly asked for. The 500 originates Toyota-side, so the fix is independent of the pytoyoda version/provider; it also complements the recent BEV climate enablement (pytoyoda f7f67eb + ea73031), which makes more vehicles reach this endpoint.

Testing

Running in production for ~1 week (since 2026-06-15) on a Corolla Touring Sports (v2.3.0, HA 2026.5.x) whose climate-settings endpoint 500s consistently. Before: refresh aborted + per-cycle TypeError spam. After: climate entity loads with sane defaults, the spam stops, and the rest of the refresh completes normally. ruff + ruff-format clean.

Compatibility

No config/schema changes. Healthy vehicles (valid climate_settings) are unaffected — guards only change behaviour on the None/500 path.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 5 complexity

Metric Results
Complexity 5

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves the robustness of the Toyota integration by catching API errors during vehicle updates and gracefully handling missing or null values in climate settings (such as when the climate-settings endpoint returns an HTTP 500). The review feedback correctly identifies a potential AttributeError in _create_climate_settings if self.vehicle.climate_settings itself is None, and provides a safer implementation using getattr.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread custom_components/toyota/climate.py Outdated
Comment on lines +233 to +234
# Start with existing operations (None when climate-settings 500'd)
ac_operations = (self.vehicle.climate_settings.operations or []).copy()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If self.vehicle.climate_settings is None (which can happen if the climate-settings endpoint completely failed or returned 500), accessing self.vehicle.climate_settings.operations directly will raise an AttributeError and crash the climate entity when a user attempts to control it. Use getattr to safely retrieve the climate_settings object and its operations attribute.

Suggested change
# Start with existing operations (None when climate-settings 500'd)
ac_operations = (self.vehicle.climate_settings.operations or []).copy()
# Start with existing operations (None when climate-settings 500'd)
climate_settings = getattr(self.vehicle, "climate_settings", None)
ac_operations = (getattr(climate_settings, "operations", None) or []).copy()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 76f8e05. climate_settings itself (not just .operations) can be None when the endpoint 500s, so I now getattr through both on the control path, matching the load-path guards elsewhere in this PR.

unsnow-iac added a commit to unsnow-iac/ha_toyota that referenced this pull request Jun 23, 2026
Addresses review feedback on pytoyoda#309: _create_climate_settings accessed
self.vehicle.climate_settings.operations directly, which AttributeErrors when
climate_settings itself is None (full climate-settings 500), crashing the
entity on a set-temperature/defrost action. getattr through both.
@unsnow-iac

Copy link
Copy Markdown
Contributor Author

Heads-up — this overlaps #310: both wrap the same vehicle.update(skip=["status"]) call (here in a try/except, #310 in an asyncio.wait_for). They're independent fixes so I kept them separate, but they nest cleanly — whichever you merge first, I'll promptly rebase the other:

try:
    await asyncio.wait_for(
        _call_tagged("vehicle.update", vin, vehicle.update(skip=["status"])),
        STATUS_FETCH_BUDGET_S,
    )
except (ToyotaApiError, ToyotaInternalError) as ex:
    ...

Happy to fold them into a single PR instead if you'd prefer.

When the /v1/global/remote/climate-settings endpoint returns HTTP 500 (seen on
some HEV/PHEV vehicles, e.g. issue pytoyoda#294), the unguarded vehicle.update() call
aborts the whole update flow. Wrap it in a try/except for ToyotaApiError /
ToyotaInternalError and log a warning so a single failing endpoint no longer
breaks the refresh.
A climate-settings HTTP 500 makes climate_settings.operations (and min/max
temp) come back None, so stock filter(..., None) raises TypeError every cycle
and defrost/temperature handling silently fails. Add None-guards in climate.py
so the climate entity degrades gracefully instead of erroring. Refs pytoyoda#294.
Addresses review feedback on pytoyoda#309: _create_climate_settings accessed
self.vehicle.climate_settings.operations directly, which AttributeErrors when
climate_settings itself is None (full climate-settings 500), crashing the
entity on a set-temperature/defrost action. getattr through both.
@unsnow-iac unsnow-iac force-pushed the fix/climate-settings-500 branch from 76f8e05 to da7de24 Compare June 24, 2026 09:44
@unsnow-iac

Copy link
Copy Markdown
Contributor Author

Rebased onto main now that #310 is merged — conflict resolved exactly as proposed above: the try/except (ToyotaApiError, ToyotaInternalError) now wraps #310's asyncio.wait_for(...), so the status fetch keeps both the setup-budget bound and the partial-failure swallow. The climate.py None-guards rebased clean (#310 didn't touch climate.py). Should be green and mergeable now.

@deejay1 deejay1 enabled auto-merge June 24, 2026 11:49
@deejay1 deejay1 merged commit 3e2caad into pytoyoda:main Jun 24, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants