feat: fix shared-vehicle collisions, add buttons, and misc bug fixes - #184
Open
thatcoleyouknow wants to merge 5 commits into
Open
feat: fix shared-vehicle collisions, add buttons, and misc bug fixes#184thatcoleyouknow wants to merge 5 commits into
thatcoleyouknow wants to merge 5 commits into
Conversation
…ehicles Entity naming: fix reversed/doubled friendly names by letting Home Assistant apply its own device-name prefix instead of building it manually. Location: telemetry's vehicleLocation now backs both RealTimeLocation and ParkingLocation (Toyota labels it "Last Parked"), fixing a dead map entry that made Last Parked Location unrecoverable on any vehicle whose REST vehicle_status doesn't report location. GraphQL confirm-subscription: use each vehicle's real backdoorType from the API instead of a hardcoded "hatch" literal, which was wrong for any non-hatch vehicle (e.g. pickup trucks report "tailgate"). diagnostics.py: fix an argument-order bug that silently sent the vehicle's generation string as the telemetry endpoint's region parameter. Remote command services: gate door_lock/unlock/engine_start/stop/hazards on each vehicle's actual remoteServiceCapabilities instead of always sending the command regardless of vehicle support. Door/lock/hatch parsing: some API responses report only a single lock-state value (e.g. "Locked") with no open/closed position, where others report two values (position then lock state). The previous code assumed a single value was always a position string, silently misreporting locked-but-unknown- position doors as "open" and losing lock state entirely. Now classifies by the actual value word instead of by array length, and reports position as genuinely unknown (entity goes unavailable) rather than fabricating a wrong state, when a response only contains lock information. Vehicle status parsing order: telemetry is now parsed before vehicle_status instead of after, so vehicle_status's fresher window/moonroof data (when present) wins instead of being silently overwritten by stale telemetry values for the same fields. Confirmed via live testing that vehicle_status and telemetry can disagree on window state even when both report identical timestamps. Zombie entity cleanup: actively remove binary_sensor entities for features a vehicle structurally cannot have (e.g. Trunk on a tailgate-body vehicle), since Home Assistant doesn't prune entities a platform stops providing. Command response logging: log the full response for remote command calls (not just HTTP errors), to make it possible to see whether a "successful" command response contains a hidden failure the vehicle never acted on. Account-permission visibility: warn explicitly when a subscribed vehicle returns no status/engine data, since this can indicate the configured account isn't the vehicle's primary remote-services driver (Toyota returns empty responses to family-shared/guest accounts) rather than an API outage. Full investigation notes, evidence, and verification steps in PLAN.md.
The WebSocket subsystem was subscribing every vehicle regardless of generation, including 17CYPLUS vehicles that work completely via REST (verified via live testing). Its own justification comment -- that REST returns "vehicle not found" for 21MM+ -- is contradicted by direct testing: REST returns rich, fresh data under a correctly-permissioned account. Left it running for 21MM/24MM as a conservative fallback rather than removing it outright, since this integration runs on other people's vehicles we can't personally verify. Required adding api_generation (the raw API generation string) to the vehicle object, since the existing `generation` property collapses 17CYPLUS/21MM/24MM into a single internal value with no way to distinguish them -- needed to know which vehicles are safe to exclude.
Adds "why" comments to the fixes in the previous two commits, for anywhere the code would otherwise look arbitrary to a future contributor: the position/lock-state value parsing is reverse- engineered from live payloads, not from any Toyota documentation; several deliberate "skip rather than guess" choices when an API response doesn't match an expected shape; a telemetry cache with no TTL that can show stale data indefinitely if an account loses access; and a couple of assumptions (single rear-cargo body style, vehicleLocation's special-cased telemetry handling) worth flagging for whoever touches this next. Also swaps a bare "17CYPLUS" string literal for the existing ApiVehicleGeneration enum's value, removing a silent-typo risk in the WebSocket-generation-filtering check from the previous commit. No behavior changes.
engine_start/stop, hazards_on/off, and refresh only ever existed as bare Home Assistant services with no entity behind them, so they never showed up as controls on the device card (unlike door_lock/unlock, which get a native Lock/Unlock control via the existing lock entity). Device cards only auto-render controls for entities, not services. Button entities are gated by vehicle.supports_command() at creation time, reusing the capability check added for service-call gating -- a vehicle that can't support a command (e.g. hazards on a truck without that capability) never gets a button for it, rather than showing a button that silently no-ops when pressed. Refresh gets its own entity class since it needs a different call path (poll_vehicle_refresh(), not send_command()) -- the existing Refresh entry in COMMAND_MAP/_command_map is dead code that was never actually reached by the working refresh path, so the button doesn't route through it either. No button for Hazards Off, deliberately, unlike every other on/off pair: live testing showed pressing it has no observable effect on a real vehicle, matching Toyota's own app (hazards there are momentary, auto-off after ~60s, with no manual "off" available even in Toyota's app). The pre-existing hazards_off *service* is left alone for automations, since this hasn't been proven broken for every vehicle/account -- just never proven working -- but a dashboard button that looks broken to every user who tries it isn't worth adding. See the comment on COMMAND_BUTTONS in const.py for the full reasoning.
A vehicle visible to two Toyota accounts (Toyota "family sharing"), each configured as its own Home Assistant config entry, previously had no protection against both entries trying to create entities for the same VIN -- whichever entry loaded second lost every entity to "already exists - ignoring", silently binding all of that vehicle's controls to the wrong account's session with no visible error. Adds a VIN claim guard (async_claim_vehicles/async_release_vehicle_ claims in __init__.py): the first config entry to see a VIN each refresh manages it: any other entry skips creating entities for it entirely and raises a Repair issue instead of colliding. This is unconditional and needs no configuration -- every existing single- account install is unaffected, and entity unique_id/device identity is untouched (still plain vin+sensor_name). For deliberate assignment rather than relying on refresh-order, adds an options flow (Settings > Devices & Services > Toyota (North America) > Configure) letting a user exclude specific vehicles from an account, so the other account's entry can claim them instead. Excluding a vehicle (or losing a claim conflict) also prunes that entry's now-unclaimed device from the registry -- scoped only to confirmed reasons (an explicit exclusion, or a claim genuinely lost this cycle), never to mere absence from a single poll, since a transient API hiccup returning a short vehicle list must never be mistaked for "this vehicle is gone" and delete real entities. async_remove_config_entry_device() covers the one case that isn't auto-pruned: a vehicle actually removed from the Toyota account entirely (e.g. sold), which the user can now delete manually from its device page. The options flow relies on OptionsFlow's automatic self.config_entry (no custom __init__), which requires Home Assistant 2024.11+ -- bumped hacs.json's declared minimum version to match.
thatcoleyouknow
added a commit
to thatcoleyouknow/ha-toyota-na
that referenced
this pull request
Jul 29, 2026
Toyota began rejecting the legacy self.API_KEY value on REST requests today (2026-07-28), returning 403 Forbidden on every call starting with get_user_vehicle_list() -- breaking the integration entirely for every account, unrelated to anything in the pending shared-vehicle-fix PR. Confirmed as a global change, not account/device/IP-specific: the same error hit multiple unrelated users on different vehicles (RAV4, bZ4X) around the same time. Switches the REST X-API-KEY header to RESOLVER_API_KEY (already used for GraphQL calls in this same file) and bumps X-APPVERSION to 3.4.0 to match Toyota's current app version. Ports the community-diagnosed fix from widewing#186 (orienw), which fixes widewing#185. Not our own discovery -- applying it here as a stopgap so this fork's users aren't stuck waiting on the unrelated PR widewing#184 to merge upstream.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A batch of fixes and one new feature found while running this integration daily against a 2020 Tacoma and a 2023 Highlander (the Highlander is shared with a second Toyota account via Toyota's family sharing). Grouped below by concern, not chronology — happy to split into separate PRs if preferred, this ended up as one because several pieces touch the same functions.
1. Mechanical bug fixes
vehicleLocationtelemetry was mapped to a dead dict entry, making "Last Parked Location" unrecoverable on any vehicle whose RESTvehicle_statusdoesn't report position.backdoorType: "hatch", wrong for non-hatch vehicles (e.g. pickup tailgates).diagnostics.pyhad an argument-order bug silently sending the vehicle's generation string as the telemetry endpoint's region parameter.remoteServiceCapabilities— the integration would send commands Toyota already reported as unsupported.vehicle_statusand telemetry can disagree on window/moonroof state for the same poll; parsing order changed so the fresher source wins.2. New: button platform
Adds real button entities for Remote Start/Stop, Hazards On, and Refresh — these previously only existed as bare services with no entity, so they never appeared as device-card controls. Buttons are gated by
supports_command()at creation time.No button for Hazards Off, deliberately: live testing showed it has no observable effect on a real vehicle, matching Toyota's own app (hazards there are momentary, auto-off after ~60s, no manual "off" even in Toyota's app). The
hazards_offservice is left alone for automations since it's never been proven broken everywhere — just never proven working.3. Fix: entity collisions when a vehicle is shared across two accounts
The actual bug that started this: a vehicle visible to two Toyota accounts (family sharing), each its own config entry, had no protection against both creating entities for the same VIN. Whichever entry loaded second silently lost every entity to "already exists - ignoring" — e.g. pressing "Hazards Off" looked like it worked but was actually bound to the wrong account's session.
unique_id/device identity is unchanged.async_remove_config_entry_devicefor the one case that isn't auto-pruned — a vehicle actually removed from the account (e.g. sold).hacs.json) since the options flow relies onOptionsFlow's automaticself.config_entryrather than a custom__init__, which HA deprecated that release.Backward compatibility
Single-account installs (the overwhelming majority) see no behavior change from item 3 at all — the claim guard and pruning logic are no-ops unless a VIN is genuinely contested. No entity
unique_idor device identifier changes anywhere in this PR.Testing
Things worth knowing as a reviewer
Assisted by Claude Code. The plan and any other details can be shared if anyone would like to review it.