Skip to content

feat: fix shared-vehicle collisions, add buttons, and misc bug fixes - #184

Open
thatcoleyouknow wants to merge 5 commits into
widewing:masterfrom
thatcoleyouknow:master-v2
Open

feat: fix shared-vehicle collisions, add buttons, and misc bug fixes#184
thatcoleyouknow wants to merge 5 commits into
widewing:masterfrom
thatcoleyouknow:master-v2

Conversation

@thatcoleyouknow

@thatcoleyouknow thatcoleyouknow commented Jul 22, 2026

Copy link
Copy Markdown

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

  • Entity friendly names showed doubled/reversed (e.g. "2023 Highlander Front Driver Door 2023 Highlander") — was building a device-name prefix manually instead of letting HA handle it.
  • vehicleLocation telemetry was mapped to a dead dict entry, making "Last Parked Location" unrecoverable on any vehicle whose REST vehicle_status doesn't report position.
  • GraphQL confirm-subscription hardcoded backdoorType: "hatch", wrong for non-hatch vehicles (e.g. pickup tailgates).
  • diagnostics.py had an argument-order bug silently sending the vehicle's generation string as the telemetry endpoint's region parameter.
  • Remote commands weren't gated on remoteServiceCapabilities — the integration would send commands Toyota already reported as unsupported.
  • Door/lock parsing assumed a single reported value was always a position string; some responses report lock-state only, silently misparsed as "open" with lock state lost entirely.
  • vehicle_status and telemetry can disagree on window/moonroof state for the same poll; parsing order changed so the fresher source wins.
  • Added cleanup for zombie binary_sensor entities created for features a vehicle structurally can't have (e.g. Trunk on a tailgate body).
  • Disabled the AppSync WebSocket for 17CYPLUS vehicles, confirmed to work fully via REST alone — left running for 21MM/24MM as a fallback since those haven't been personally verified.

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_off service 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.

  • VIN claim guard: the first config entry to see a VIN each refresh manages it; any other entry skips entity creation for it and raises a Repair issue instead of colliding. Unconditional, no config needed — single-account installs are unaffected, and entity unique_id/device identity is unchanged.
  • Options flow (Configure): lets a user explicitly exclude a vehicle from an account so the other account's entry can claim it deliberately, instead of by refresh-order.
  • Device pruning: excluding a vehicle (or losing a claim conflict) removes that entry's now-unclaimed device — but only for confirmed reasons (explicit exclusion, or a claim genuinely lost that cycle), never from a vehicle merely missing from one poll, since a transient API hiccup returning a short vehicle list must never be mistaken for "this vehicle is gone" and delete real entities. (This distinction was added after an internal review caught the first version of this pruning logic doing exactly that.)
  • Manual device removal: added async_remove_config_entry_device for the one case that isn't auto-pruned — a vehicle actually removed from the account (e.g. sold).
  • Requires Home Assistant 2024.11+ (bumped in hacs.json) since the options flow relies on OptionsFlow's automatic self.config_entry rather 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_id or device identifier changes anywhere in this PR.

Testing

  • Every parsing fix verified against live API payloads (diffed a working vs. a permission-degraded account's response for the same vehicle).
  • Live-tested lock/unlock/engine start/stop/refresh against two real vehicles.
  • Live-tested the shared-vehicle fix against two real Toyota accounts sharing one vehicle: confirmed the collision is gone and commands now route to the correct account.
  • Claim-guard, pruning, and issue-lifecycle logic covered by standalone state-machine simulations (conflict, steady state, release, re-claim, and the "loser removed without ever claiming" edge case).
  • Ran with debug logging on for ~2.5 days of normal daily use on my own instance (deployed via a custom HACS build ahead of this PR, not merged anywhere upstream): 591/592 successful refreshes (the one failure was a transient network error reaching Toyota's own auth server, unrelated to this PR, self-healed on the next retry), zero entity collisions, zero reload storms despite ~590 real token-refresh writes to entry.data (confirming the reload-listener fix holds under real load, not just in a test).

Things worth knowing as a reviewer

  • A Repair issue can go stale until the losing entry's next reload if sharing is revoked mid-session — self-heals on restart, not fixed proactively (deemed not worth the extra complexity).
  • A losing entry logs a WARNING every ~10 min until the conflict is resolved — semi-intentional, gives the user a reason to act.
  • The options flow does a second Toyota API call on submit (fetches the vehicle list twice across the flow) — minor, not fixed.

Assisted by Claude Code. The plan and any other details can be shared if anyone would like to review it.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant