Skip to content

Instantly share code, notes, and snippets.

@nledenyi
Last active June 16, 2026 05:14
Show Gist options
  • Select an option

  • Save nledenyi/2e8a08af6014713384eef9db05fd3562 to your computer and use it in GitHub Desktop.

Select an option

Save nledenyi/2e8a08af6014713384eef9db05fd3562 to your computer and use it in GitHub Desktop.
Diagnostic probe for Toyota climate-related issues (ha_toyota#246, #291) - dumps capability flags + tries climate endpoints

pytoyoda diagnostic probe (v2)

A read-only diagnostic for accounts hitting climate / status issues with the Toyota HA integration. Dumps feature flags, account-state gating signals, and a sanitized view of what your vehicle returns from each climate endpoint.

The output is sanitised - VINs are truncated to the last 6 chars, GPS is redacted, strings are capped. It's safe to paste verbatim as a comment on this gist.

What it does

For each vehicle on your MyToyota account it prints:

  • The True flags from features, extended_capabilities, and remote_service_capabilities.
  • The remoteDisplay value (account-state enum: ACTIVATED / FAILED / SUBSCRIPTION_EXPIRED / etc.).
  • A "predicted climate class" derived from the flags.
  • Three GET tries each on /climate-settings and /climate-status, with a sanitised first-try payload.
  • A single POST /refresh-climate-status (the wake call - same one the official MyT app sends; idempotent).
  • A re-read of both endpoints after the wake, to see if anything changed.

It does not start the engine, send climate-control commands, or modify saved settings. The only POST it issues is the idempotent wake.

Prerequisites

  • A working Toyota integration on Home Assistant (the script reuses the credentials you already configured - no need to re-enter them).
  • A way to run a Python script inside the homeassistant container. Easiest: the Advanced SSH & Web Terminal add-on (with Protection Mode off), or the VS Code add-on's terminal.

Run it

  1. Download the script into /config/:
    wget -O /config/probe-toyota-climate.py \
      https://gist.githubusercontent.com/nledenyi/2e8a08af6014713384eef9db05fd3562/raw/probe-toyota-climate.py
  2. Run it:
    docker exec homeassistant python3 /config/probe-toyota-climate.py
    Or filter to a single vehicle alias (case-insensitive substring match):
    docker exec homeassistant python3 /config/probe-toyota-climate.py rav4
    Or run and save to a file:
    docker exec homeassistant python3 /config/probe-toyota-climate.py > /config/probe-output.txt

What to share

Paste the entire output as a comment. If anything looks personal that the sanitiser missed, redact it before posting and let me know what was found.

Bugs / questions

Drop a comment on this gist. The script is short - if you'd rather inspect it before running, the source is right here below.

import asyncio
import json
import sys
from loguru import logger
logger.remove()
from pytoyoda import MyT
from pytoyoda.const import (
VEHICLE_CLIMATE_SETTINGS_ENDPOINT,
VEHICLE_CLIMATE_STATUS_ENDPOINT,
)
TRIES = 3
REFRESH_CLIMATE_ENDPOINT = "/v1/global/remote/refresh-climate-status"
# RemoteDisplayStatus enum from APK (oneapp/domain/dto/vehicle/VehicleGeneral.java).
# backendValue == ordinal (verified). Only ACTIVATED is the green-light state;
# all other values cause the MyT app to skip remote calls.
REMOTE_DISPLAY_NAMES = {
0: "UNKNOWN",
1: "AUTH_REQUIRED",
2: "SUBSCRIPTION_CANCELLED_REMOTE_USER",
3: "SUBSCRIPTION_CANCELLED_PRIMARY_USER",
4: "FAILED",
5: "PENDING",
6: "ERROR",
7: "ACTIVATED",
8: "SUBSCRIPTION_EXPIRED_REMOTE_USER",
9: "SUBSCRIPTION_EXPIRED_PRIMARY_USER",
10: "STOLEN_LOST_VEHICLE",
}
def short(vin):
return f"...{vin[-6:]}" if vin else "<no-vin>"
def cap_str(s, n=200):
if isinstance(s, str) and len(s) > n:
return s[:n] + f"...({len(s)} chars)"
return s
def sanitize(obj, depth=0):
"""Recursive sanitize. Drop GPS, cap strings, cap lists."""
if depth > 8:
return "<deep>"
if isinstance(obj, dict):
out = {}
for k, v in obj.items():
kl = k.lower()
if kl in (
"latitude",
"longitude",
"lat",
"lon",
"lng",
"gps_lat",
"gps_lon",
"start_lat",
"start_lon",
"end_lat",
"end_lon",
):
out[k] = "<redacted-gps>"
elif kl in ("vin", "vehicle_id", "guid", "uuid"):
out[k] = short(v) if isinstance(v, str) else v
else:
out[k] = sanitize(v, depth + 1)
return out
if isinstance(obj, list):
if len(obj) > 10:
return [sanitize(x, depth + 1) for x in obj[:10]] + [
f"<...{len(obj) - 10} more>"
]
return [sanitize(x, depth + 1) for x in obj]
if isinstance(obj, str):
return cap_str(obj)
return obj
def predict_climate_class(features, ext):
"""Derive expected climate behavior from feature flags."""
cse = getattr(features, "climate_start_engine", False)
cc = getattr(ext, "climate_capable", False)
ctf = getattr(ext, "climate_temperature_control_full", False)
ctl = getattr(ext, "climate_temperature_control_limited", False)
ecc = getattr(ext, "econnect_climate_capable", False)
res = getattr(ext, "remote_engine_start_stop", False)
if cc and (ctf or ctl):
return "FULL_CLIMATE", (
"set target temp + on/off; "
"POST /climate-control body: {command: 'engine-start', remoteHvac: {...}}; "
"PUT /climate-settings updates ACOperations + temperature"
)
if cc and not (ctf or ctl):
return "CLIMATE_NO_TEMP", (
"on/off + ACOperations toggle; no target temp; "
"PUT /climate-settings updates ACOperations only"
)
if res:
return "ENGINE_PREHEAT", (
"on/off only via engine-start; auto-off after ~20 min; "
"POST /climate-control body: {command: 'engine-start'} (no remoteHvac)"
)
if ecc:
return "ECONNECT", (
"Stellantis-derived variant; not enough data on shape; "
"treat like FULL_CLIMATE pending more probe data"
)
if cse:
return "LEGACY_FLAG", (
"features.climate_start_engine=True is the old gate; "
"behavior depends on which extended_capabilities flags are also True"
)
return "NO_CLIMATE", "no remote climate flags True; skip all climate calls"
async def call_one(api, method, endpoint, vin, body=None, label=""):
"""Hit an endpoint via the controller's request_raw. Returns (status, body)."""
try:
resp = await api.controller.request_raw(
method=method, endpoint=endpoint, vin=vin, body=body
)
try:
j = resp.json()
except Exception:
j = None
return resp.status_code, j
except Exception as ex:
msg = str(ex)
if len(msg) > 280:
msg = msg[:280] + "..."
return None, f"EXCEPTION {type(ex).__name__}: {msg}"
def render_payload(label, status, body):
if status is None:
print(f" {label}: {body}")
return
if 200 <= status < 300 and isinstance(body, dict):
payload = body.get("payload")
present = bool(payload) and payload != {}
print(f" {label}: HTTP {status} payload_present={present}")
if present:
sanitized = sanitize(payload)
blob = json.dumps(sanitized, indent=6, default=str)
if len(blob) > 2000:
blob = blob[:2000] + f"\n <...{len(blob) - 2000} more chars>"
print(f" sanitized_payload:\n{blob}")
else:
# Non-2xx or non-dict: dump status + first 280 of body
body_repr = json.dumps(body, default=str)[:280] if body is not None else ""
print(f" {label}: HTTP {status} {body_repr}")
async def probe_vehicle(v):
info = v._vehicle_info
vin = v.vin
print(f"\n=== {v.alias} ({short(vin)}) ===")
print(f" Model: {getattr(info, 'car_model_name', '?')}")
# Flag dump (truncated to True flags only, for compactness)
feats = info.features
ext = info.extended_capabilities
rsc = info.remote_service_capabilities
feats_true = sorted(k for k, v in feats.model_dump().items() if v is True)
ext_true = sorted(k for k, v in ext.model_dump().items() if v is True)
rsc_true = sorted(k for k, v in rsc.model_dump().items() if v is True)
print(f" features TRUE ({len(feats_true)}): {feats_true}")
print(f" extended_capabilities TRUE ({len(ext_true)}): {ext_true}")
print(f" remote_service_capabilities TRUE ({len(rsc_true)}): {rsc_true}")
# Account-state gating signals (NEW - APK uses RemoteDisplayStatus enum
# to gate remote calls beyond the features.remote_service boolean).
# Only ACTIVATED (7) is the green light.
remote_display = getattr(info, "remote_display", "<missing>")
remote_service = getattr(feats, "remote_service", None)
rd_type = type(remote_display).__name__
if isinstance(remote_display, int):
rd_name = REMOTE_DISPLAY_NAMES.get(remote_display, f"<unknown int {remote_display}>")
elif isinstance(remote_display, str) and remote_display.isdigit():
rd_name = REMOTE_DISPLAY_NAMES.get(int(remote_display), f"<unknown int {remote_display}>")
elif isinstance(remote_display, str):
rd_name = remote_display
else:
rd_name = "<non-int, see raw>"
print(f" remoteDisplay: {sanitize(remote_display)} (type={rd_type}, decoded={rd_name})")
print(f" features.remote_service: {remote_service}")
# Predicted climate class
klass, hint = predict_climate_class(feats, ext)
print(f" Predicted climate class: {klass}")
print(f" -> {hint}")
api = v._api
# READ tests (3 tries each)
print(f"\n Pre-refresh reads (3 tries each):")
for label, ep in [
("climate_settings", VEHICLE_CLIMATE_SETTINGS_ENDPOINT),
("climate_status", VEHICLE_CLIMATE_STATUS_ENDPOINT),
]:
for i in range(1, TRIES + 1):
status, body = await call_one(api, "GET", ep, vin, label=label)
tag = f"try {i}/{TRIES}"
if i == 1:
# Only render full payload on the first try; subsequent are smoke
render_payload(f"{label} {tag}", status, body)
else:
if isinstance(body, dict):
payload = body.get("payload")
present = bool(payload) and payload != {}
print(f" {label} {tag}: HTTP {status} payload_present={present}")
else:
print(f" {label} {tag}: HTTP {status} {body}")
# WAKE: POST refresh-climate-status (idempotent)
print(f"\n Wake POST (NEW, not in v1):")
status, body = await call_one(api, "POST", REFRESH_CLIMATE_ENDPOINT, vin)
if status is None:
# Exception path - body holds the EXCEPTION ... string from call_one
print(f" refresh-climate-status: HTTP None ({body})")
else:
print(f" refresh-climate-status: HTTP {status}")
if isinstance(body, dict):
msgs = body.get("status", {}).get("messages", []) if isinstance(body.get("status"), dict) else []
for m in msgs[:3]:
print(f" {m.get('responseCode', '?')}: {m.get('description', '?')}")
# On non-2xx with a parsed body, also dump first 280 chars
if not (200 <= status < 300):
print(f" body: {json.dumps(body, default=str)[:280]}")
# Wait briefly to give the modem a chance to push fresh climate-status
await asyncio.sleep(8)
# Re-read to see if things changed
print(f"\n Post-refresh reads (1 try each, full payload dump):")
for label, ep in [
("climate_settings", VEHICLE_CLIMATE_SETTINGS_ENDPOINT),
("climate_status", VEHICLE_CLIMATE_STATUS_ENDPOINT),
]:
status, body = await call_one(api, "GET", ep, vin, label=label)
render_payload(label, status, body)
async def main():
# Reuse HA's stored credentials, same pattern as v1.
try:
with open("/config/.storage/core.config_entries") as f:
stored = json.load(f)
except FileNotFoundError:
print("Could not find /config/.storage/core.config_entries", file=sys.stderr)
sys.exit(1)
creds = next(
(e["data"] for e in stored["data"]["entries"] if e.get("domain") == "toyota"),
None,
)
if not creds or "email" not in creds or "password" not in creds:
print("No toyota config entry with credentials found.", file=sys.stderr)
sys.exit(2)
brand_map = {"toyota": "T", "lexus": "L"}
brand_code = brand_map.get(creds.get("Brand", "toyota"), "T")
filter_alias = sys.argv[1].lower() if len(sys.argv) > 1 else None
print(f"pytoyoda climate probe v2")
print(f"pytoyoda version: {__import__('pytoyoda').__version__}")
print(f"Brand: {creds.get('Brand', 'toyota')} (code: {brand_code})")
if filter_alias:
print(f"Filter: only vehicles whose alias contains '{filter_alias}'")
print()
client = MyT(
username=creds["email"], password=creds["password"], brand=brand_code
)
await client.login()
vehicles = await client.get_vehicles()
print(f"Found {len(vehicles)} vehicle(s).")
for v in vehicles:
if filter_alias and filter_alias not in v.alias.lower():
continue
await probe_vehicle(v)
if __name__ == "__main__":
asyncio.run(main())
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Toyota probe v2 results</title>
<style>
:root { color-scheme: light dark; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 1200px; margin: 0 auto; padding: 1.5rem; line-height: 1.5; }
h1 { margin-top: 0; }
h2 { border-bottom: 1px solid #ccc; padding-bottom: 0.3rem; margin-top: 2rem; }
table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: 0.92rem; }
th, td { padding: 0.5rem 0.7rem; border: 1px solid #ccc; text-align: left; vertical-align: top; }
th { background: rgba(127,127,127,0.1); font-weight: 600; }
tr:nth-child(even) { background: rgba(127,127,127,0.04); }
td.ok { color: #167c2c; font-weight: 600; }
td.warn { color: #a06800; font-weight: 600; }
td.err { color: #b62020; font-weight: 600; }
td.muted { color: #888; }
details { background: rgba(127,127,127,0.06); border: 1px solid #ccc; border-radius: 4px;
margin: 0.5rem 0 1.5rem 0; padding: 0.5rem 0.8rem; }
details summary { cursor: pointer; font-weight: 600; padding: 0.3rem 0; }
details[open] summary { border-bottom: 1px solid #ccc; margin-bottom: 0.5rem; }
pre { background: rgba(0,0,0,0.04); padding: 0.7rem; border-radius: 4px; overflow-x: auto;
font-size: 0.82rem; line-height: 1.4; white-space: pre-wrap; word-break: break-all; }
.legend { font-size: 0.85rem; color: #666; margin: 0.5rem 0 1.5rem 0; }
.meta { font-size: 0.85rem; color: #666; margin-bottom: 1.5rem; }
code { background: rgba(127,127,127,0.15); padding: 0.1rem 0.3rem; border-radius: 3px;
font-size: 0.88em; }
@media (prefers-color-scheme: dark) {
body { background: #1a1a1a; color: #e0e0e0; }
th, td { border-color: #444; }
pre { background: rgba(255,255,255,0.05); }
td.ok { color: #6cc587; }
td.warn { color: #d8a040; }
td.err { color: #e57878; }
}
</style>
</head>
<body>
<h1>Toyota probe v2 results</h1>
<p class="meta">
Aggregated outputs from
<a href="https://github.com/pytoyoda/ha_toyota/issues/296">ha_toyota#296</a>
+ maintainer's own runs. Source script + instructions:
<a href="https://gist.github.com/nledenyi/2e8a08af6014713384eef9db05fd3562">gist 2e8a08af</a>.
Last updated: 2026-05-05 evening (10 vehicles).
</p>
<h2>Summary</h2>
<p class="legend">
Click a vehicle name to jump to its full sanitised probe output below.
Colour: <span style="color:#167c2c"><b>green</b></span> = healthy 200,
<span style="color:#a06800"><b>amber</b></span> = empty / stub / limited,
<span style="color:#b62020"><b>red</b></span> = error / 500 / 400.
</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Tester</th>
<th>Vehicle</th>
<th>Drivetrain</th>
<th><code>remoteDisplay</code></th>
<th>Predicted class</th>
<th>eConnect surface*</th>
<th><code>GET /climate-settings</code></th>
<th><code>GET /climate-status</code></th>
<th><code>POST /refresh-climate-status</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>nledenyi</td>
<td><a href="#a-rav4">RAV4 '19</a></td>
<td>hybrid</td>
<td class="warn">9 EXPIRED_PRIMARY_USER</td>
<td>NO_CLIMATE</td>
<td>none</td>
<td class="warn">200 empty</td>
<td class="warn">200 type:basic status:0 (stub)</td>
<td class="err">400 <code>RS-10068</code> "feature inactive"</td>
</tr>
<tr>
<td>B</td>
<td>nledenyi</td>
<td><a href="#b-aygo">AYGO X '22</a></td>
<td>ICE/MHEV</td>
<td class="ok">7 ACTIVATED</td>
<td>ENGINE_PREHEAT</td>
<td>none</td>
<td class="ok">200 populated (temp:null, range 18-29)</td>
<td class="ok">200 type:basic status:0</td>
<td class="ok">200 OK</td>
</tr>
<tr>
<td>C</td>
<td>sciurius</td>
<td><a href="#c-corolla">Corolla HB/TS '23</a></td>
<td>hybrid</td>
<td class="ok">7 ACTIVATED</td>
<td>FULL_CLIMATE</td>
<td>none</td>
<td class="err">500 <code>RS-40000</code> (3/3 + post-wake)</td>
<td class="ok">200 type:full status:0</td>
<td class="ok">200 OK (does NOT clear settings 500)</td>
</tr>
<tr>
<td>D</td>
<td>Paja-git</td>
<td><a href="#d-lexus-nx">Lexus NX450h+ '21</a></td>
<td><b>PHEV</b></td>
<td class="warn">9 EXPIRED_PRIMARY_USER</td>
<td>FULL_CLIMATE</td>
<td><b>full</b></td>
<td class="ok">200 populated (temp:22, defrost + seat heat)</td>
<td class="ok">200 type:full status:0</td>
<td class="ok">200 OK</td>
</tr>
<tr>
<td>E</td>
<td>ArnstadFredrik</td>
<td><a href="#e-bz4x">BZ4X '22</a></td>
<td><b>BEV</b></td>
<td class="ok">7 ACTIVATED</td>
<td>FULL_CLIMATE</td>
<td><b>full</b></td>
<td class="ok">200 populated (temp:20, defrost ops)</td>
<td class="ok">200 type:full status:0</td>
<td class="ok">200 OK</td>
</tr>
<tr>
<td>F</td>
<td>voyagers21</td>
<td><a href="#f-yaris">Yaris '25</a></td>
<td>hybrid</td>
<td class="ok">7 ACTIVATED</td>
<td>FULL_CLIMATE</td>
<td>none</td>
<td class="err">500 <code>RS-40000</code> (3/3 + post-wake)</td>
<td class="err">500 <code>RS-40000</code> (3/3 + post-wake)</td>
<td class="ok">200 OK (does NOT clear either 500)</td>
</tr>
<tr>
<td>G</td>
<td>Nickduino</td>
<td><a href="#g-lexus-ux">Lexus UX250h '19</a></td>
<td>hybrid</td>
<td class="warn">9 EXPIRED_PRIMARY_USER</td>
<td>NO_CLIMATE</td>
<td>none</td>
<td class="warn">200 empty</td>
<td class="warn">200 type:basic status:0 (stub)</td>
<td class="err">400 <code>RS-10068</code> "feature inactive"</td>
</tr>
<tr>
<td>H</td>
<td>jmalvarezf-lmes</td>
<td><a href="#h-corolla-ng19">Corolla TS NG'19</a></td>
<td>hybrid</td>
<td class="warn">9 EXPIRED_PRIMARY_USER</td>
<td>NO_CLIMATE</td>
<td>none</td>
<td class="warn">200 empty</td>
<td class="warn">200 type:basic status:0 (stub)</td>
<td class="err">400 <code>RS-10068</code> "feature inactive"</td>
</tr>
<tr>
<td>I</td>
<td>Buschi145</td>
<td><a href="#i-corolla-buschi">Corolla HB/TS '23 #2</a></td>
<td>hybrid</td>
<td class="ok">7 ACTIVATED</td>
<td>FULL_CLIMATE</td>
<td>none</td>
<td class="err">500 <code>RS-40000</code> (3/3 + post-wake)</td>
<td class="err">500 <code>RS-40000</code> (3/3 + post-wake)</td>
<td class="ok">200 OK (does NOT clear either 500)</td>
</tr>
<tr>
<td>J</td>
<td>Eddie84</td>
<td><a href="#j-rav4-phev-eddie">RAV4 PHEV '23</a></td>
<td><b>PHEV</b></td>
<td class="ok">7 ACTIVATED</td>
<td>ECONNECT</td>
<td><b>full</b></td>
<td class="ok">200 populated (temp:18, defrost only)</td>
<td class="ok">200 type:full status:0</td>
<td class="ok">200 OK</td>
</tr>
</tbody>
</table>
<p class="legend">
*<b>eConnect surface</b>: shorthand for the cluster of extended_capabilities flags
<code>econnect_vehicle_status_capable</code>, <code>remote_econnect_capable</code>,
<code>c_scheduling</code>, <code>charge_management</code>, plus seat-heater capabilities.
Present on BEV (E) and PHEVs (D, J); absent on every pure hybrid we have data for
(A, C, F, G, H, I) and the ICE-MHEV AYGO X (B).
</p>
<h2>Current working hypothesis</h2>
<p><b>The eConnect / charging capability surface predicts whether a FULL_CLIMATE
vehicle can read its climate endpoints.</b> Vehicles whose
<code>extended_capabilities</code> include the cluster
<code>econnect_vehicle_status_capable</code> +
<code>remote_econnect_capable</code> + <code>c_scheduling</code> +
<code>charge_management</code> + seat-heater flags (BEVs and PHEVs) return
populated 200s on both <code>/climate-settings</code> and
<code>/climate-status</code>. FULL_CLIMATE vehicles without that surface
(pure-hybrid drivetrains) return <code>500 RS-40000 "Command execution
interrupted"</code> on <code>/climate-settings</code> and sometimes also on
<code>/climate-status</code>. The wake POST and the subscription state
(<code>remoteDisplay</code>) do not change this outcome.</p>
<p><b>Score across the dataset:</b> 3/3 vehicles with the eConnect surface return
clean 200s (D, E, J). 3/3 FULL_CLIMATE vehicles without it return 500 on at least
<code>/climate-settings</code> (C, F, I); F and I additionally 500 on
<code>/climate-status</code>. NO_CLIMATE and ENGINE_PREHEAT classes (A, B, G, H)
are out of scope for this hypothesis - they don't trip <code>RS-40000</code> at
all.</p>
<p><b>Falsifying datapoints would be:</b> a FULL_CLIMATE pure-hybrid (no
eConnect surface) that returns 200, OR a FULL_CLIMATE BEV/PHEV (with eConnect
surface) that 500s. None observed across 10 vehicles.</p>
<p><b>Sub-finding (NEW 2026-05-05, ArnstadFredrik + Eddie84):</b> the gateway
<b>under-reports <code>acOperations</code></b> relative to what the MyT app
controls. ArnstadFredrik's BZ4X probe returns only <code>defrost</code> in
<code>acOperations</code>, but the MyT app exposes front-driver-seat heater +
front-passenger-seat heater + steering-wheel heater. Eddie84's RAV4 PHEV
(vehicle J) shows the same shape - <code>defrost</code> only in
<code>acOperations</code>, despite being an eConnect/PHEV. Implication for the
integration: <code>acOperations</code> is not a reliable source-of-truth for
the user's controllable surface; the actual control set is broader. A MyT
<code>POST /climate-control</code> body capture would tell us where MyT reads
the full set from.</p>
<p><b>Descriptive findings (not part of the hypothesis):</b></p>
<ul>
<li><code>type: "full"</code> vs <code>"basic"</code> on
<code>/climate-status</code> tracks climate class - "full" appears on
every FULL_CLIMATE vehicle that returns a 200 (C, D, E); "basic" appears
on every NO_CLIMATE / ENGINE_PREHEAT vehicle (A, B, G, H).</li>
<li>Same model code, different blast radius: C and I are both Corolla HB/TS
MC '23, but C 500s on <code>/climate-settings</code> only while I 500s
on both endpoints. The variable that drives this isn't visible in the
capability flags - looks like per-VIN data state.</li>
<li>Expired subscription (<code>remoteDisplay=9</code>) degrades reads on
NO_CLIMATE accounts to 200-empty <code>/climate-settings</code> +
<code>type:basic</code> stub <code>/climate-status</code> + 400
<code>RS-10068</code> on the wake POST. Same shape across Toyota and
Lexus brands.</li>
<li><b>Implication for pytoyoda#258:</b> "treat as optional" aligns with
what other EU OneApp clients (evcc, tojota) do - they don't call
<code>/climate-settings</code> at all. F + I's data argues for
extending the same flag to <code>/climate-status</code>.</li>
</ul>
<h2>APK reverse-engineering vs probe data</h2>
<p>The probe was designed against the Toyota OneApp Android APK
(<code>co/o.java</code> Retrofit interface and the
<code>oneapp.domain.dto.vehicle.remote</code> + <code>VehicleGeneral.java</code>
data classes). What the APK tells us, and how the wire reality matches:</p>
<table>
<thead>
<tr>
<th>APK finding</th>
<th>Wire reality from probe data</th>
<th>Match?</th>
</tr>
</thead>
<tbody>
<tr>
<td><b>Five climate endpoints under <code>/v1/global/remote/climate-*</code></b>:
<code>GET /climate-settings</code>, <code>GET /climate-status</code>,
<code>POST /climate-control</code>, <code>POST /refresh-climate-status</code>,
<code>PUT /climate-settings</code>. No vehicle-class-specific routing -
every vehicle hits the same paths.</td>
<td>Confirmed for the three endpoints the probe exercises (the two GETs and
the wake POST). All vehicles regardless of class hit the same paths;
class differentiation lives in body shape and HTTP status.</td>
<td class="ok">Match</td>
</tr>
<tr>
<td><b><code>ClimateSettingsPayload</code></b> shape: <code>temperature</code>,
<code>minTemp</code>, <code>maxTemp</code>, <code>tempInterval</code>,
<code>temperatureUnit</code>, <code>acOperations[]</code>,
<code>settingsOn</code>.</td>
<td>D and E return exactly this shape. <code>acOperations</code> contains
structured category/parameter trees (defrost, seatHeat, etc.) on PHEVs
and BEVs. AYGO X (B, ENGINE_PREHEAT) returns the same fields with
<code>temperature: null</code> and <code>acOperations: []</code>.</td>
<td class="ok">Match</td>
</tr>
<tr>
<td><b><code>ClimateStatusModel</code></b> with a <code>type</code>
discriminator field.</td>
<td>Probe data confirms two values in the wild: <code>"full"</code> for
FULL_CLIMATE vehicles, <code>"basic"</code> for NO_CLIMATE /
ENGINE_PREHEAT. APK didn't enumerate the values; probe filled them in.</td>
<td class="ok">Match (extended)</td>
</tr>
<tr>
<td><b><code>RemoteClimateControl</code> enum</b> in
<code>oneapp.domain.dto.vehicle.remote</code> declares only
<code>ENGINE_START</code> ("engine-start") and <code>ENGINE_STOP</code>
("engine-stop"). Full-HVAC mode/temp settings live in an optional
<code>remoteHvac</code> object on the same body.</td>
<td>Not exercised by the probe (no climate write attempts) - so neither
confirmed nor falsified yet. Worth a future probe extension if we want
to characterise the <code>remoteHvac</code> body shape.</td>
<td class="warn">Untested</td>
</tr>
<tr>
<td><b><code>RemoteDisplayStatus</code> enum</b> from
<code>VehicleGeneral.java</code> - 11 values with
<code>backendValue</code> matching ordinal (0 UNKNOWN ...
<b>7 ACTIVATED</b> ... 9 SUBSCRIPTION_EXPIRED_PRIMARY_USER ...
10 STOLEN_LOST_VEHICLE).</td>
<td>Wire shape is <code>str</code>, not <code>int</code> - APK declares it as
an enum-int but the JSON serialises the ordinal as a string ("7", "9", ...).
Pytoyoda's <code>remote_display</code> field is typed
<code>Any</code> and would need <code>StrEnum</code> coercion if promoted.
Observed values: 7 (ACTIVATED) on B, C, E, F, I; 9
(SUBSCRIPTION_EXPIRED_PRIMARY_USER) on A, D, G, H.</td>
<td class="ok">Match (with type-coercion caveat)</td>
</tr>
<tr>
<td><b>MyT app gates remote actions on
<code>vehicleRemoteService AND vehicleRemoteDisplay == ACTIVATED</code></b>
(per <code>DashboardFullRemoteUC</code>). So
<code>features.remote_service=True</code> alone is necessary but not
sufficient.</td>
<td>Confirmed for the wake POST: every <code>remoteDisplay=9</code> account
(A, G, H) gets <code>400 RS-10068 "Remote Service feature is inactive"</code>
regardless of <code>features.remote_service</code>. ACTIVATED accounts
get <code>200 RS-10000</code>. So the gate the MyT app applies client-side
is also enforced server-side on writes.</td>
<td class="ok">Match</td>
</tr>
<tr>
<td><b>Implicit assumption from APK</b>: <code>RemoteDisplayStatus=ACTIVATED</code>
is necessary for endpoints to return useful data; non-ACTIVATED states
cause the MyT app to skip remote calls entirely.</td>
<td>Partially correct. Non-ACTIVATED state degrades reads to empty payloads,
not 500s (A, G, H). And ACTIVATED state does <i>not</i> guarantee 200 -
C, F, I are all ACTIVATED and 500. So the APK's gating is necessary
but insufficient: there's a separate, server-side per-VIN failure mode
that the APK doesn't reveal.</td>
<td class="warn">Partial</td>
</tr>
<tr>
<td><b>Not in APK</b>: any explicit signal that the eConnect /
charging-management capability surface affects the climate read path.
The APK's climate endpoints are uniform across vehicle classes; the
capability flags are documented as UI-gating hints
(<code>climate_capable</code>, <code>climate_temperature_control_full</code>,
etc.) for whether to render certain controls.</td>
<td>Probe data shows the eConnect surface is the strongest predictor of
whether a FULL_CLIMATE vehicle's reads work or 500. This is <b>net-new
information from the probe</b> - the APK doesn't suggest a relationship,
so the cause is presumably backend-side (different code path or schema
handling for vehicles in the eConnect ecosystem).</td>
<td class="warn">New finding</td>
</tr>
</tbody>
</table>
<p><b>Net learnings beyond what the APK reveals:</b> (1) the wire format of
<code>remoteDisplay</code> is <code>str</code>; (2) the
<code>type</code> discriminator on <code>/climate-status</code> takes the values
"full" and "basic"; (3) FULL_CLIMATE pure-hybrid accounts hit a server-side 500
that the APK's gating logic doesn't anticipate, and the eConnect capability
surface predicts who is affected.</p>
<h2 id="a-rav4">A &middot; RAV4 '19 (nledenyi)</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0.post2.dev0+6604c0c
Brand: toyota (code: T)
Filter: only vehicles whose alias contains 'rav4'
Found 2 vehicle(s).
=== RAV4 (...012600) ===
Model: Rav4 - NG '19
features TRUE (8): ['dealer_appointment', 'hybrid_pulse', 'last_parked', 'privacy', 'service_history', 'telemetry', 'vehicle_diagnostic', 'vehicle_status']
extended_capabilities TRUE (22): ['bonnet_status', 'front_driver_door_lock_status', 'front_driver_door_open_status', 'front_driver_door_window_status', 'front_passenger_door_lock_status', 'front_passenger_door_open_status', 'front_passenger_door_window_status', 'fuel_level_available', 'fuel_range_available', 'hybrid_pulse', 'light_status', 'rear_driver_door_lock_status', 'rear_driver_door_open_status', 'rear_driver_door_window_status', 'rear_hatch_rear_window', 'rear_passenger_door_lock_status', 'rear_passenger_door_open_status', 'rear_passenger_door_window_status', 'smart_key_status', 'telemetry_capable', 'vehicle_diagnostic_capable', 'vehicle_status']
remote_service_capabilities TRUE (0): []
remoteDisplay: 9 (type=str, decoded=SUBSCRIPTION_EXPIRED_PRIMARY_USER)
features.remote_service: False
Predicted climate class: NO_CLIMATE
-&gt; no remote climate flags True; skip all climate calls
Pre-refresh reads (3 tries each):
climate_settings try 1/3: HTTP 200 payload_present=False
climate_settings try 2/3: HTTP 200 payload_present=False
climate_settings try 3/3: HTTP 200 payload_present=False
climate_status try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "basic",
"status": 0
}
climate_status try 2/3: HTTP 200 payload_present=True
climate_status try 3/3: HTTP 200 payload_present=True
Wake POST (NEW, not in v1):
refresh-climate-status: HTTP None (EXCEPTION ToyotaApiError: Request Failed. 400, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-10068","description":"Remote Service feature is inactive for the vin","detailedDescription":"Remote Service feature is inactive for the vin"}]}}.)
Post-refresh reads (1 try each, full payload dump):
climate_settings: HTTP 200 payload_present=False
climate_status: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "basic",
"status": 0
}
</pre>
</details>
<h2 id="b-aygo">B &middot; AYGO X '22 (nledenyi)</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0.post2.dev0+6604c0c
Brand: toyota (code: T)
Filter: only vehicles whose alias contains 'aygo'
Found 2 vehicle(s).
=== AYGO (...077308) ===
Model: Aygo X - NG '22
features TRUE (10): ['dealer_appointment', 'drive_pulse', 'last_parked', 'my_destination', 'privacy', 'remote_service', 'service_history', 'telemetry', 'vehicle_diagnostic', 'vehicle_status']
extended_capabilities TRUE (28): ['bonnet_status', 'door_lock_unlock_capable', 'drive_pulse', 'front_driver_door_lock_status', 'front_driver_door_open_status', 'front_driver_door_window_status', 'front_passenger_door_lock_status', 'front_passenger_door_open_status', 'front_passenger_door_window_status', 'fuel_level_available', 'fuel_range_available', 'guest_driver', 'hazard_capable', 'last_parked_capable', 'light_status', 'rear_driver_door_lock_status', 'rear_driver_door_open_status', 'rear_driver_door_window_status', 'rear_hatch_rear_window', 'rear_passenger_door_lock_status', 'rear_passenger_door_open_status', 'rear_passenger_door_window_status', 'remote_engine_start_stop', 'smart_key_status', 'telemetry_capable', 'vehicle_diagnostic_capable', 'vehicle_finder', 'vehicle_status']
remote_service_capabilities TRUE (0): []
remoteDisplay: 7 (type=str, decoded=ACTIVATED)
features.remote_service: True
Predicted climate class: ENGINE_PREHEAT
-&gt; on/off only via engine-start; auto-off after ~20 min; POST /climate-control body: {command: 'engine-start'} (no remoteHvac)
Pre-refresh reads (3 tries each):
climate_settings try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"temperature": null,
"temperatureUnit": null,
"minTemp": 18.0,
"maxTemp": 29.0,
"tempInterval": 1.0,
"acOperations": [],
"settingsOn": true
}
climate_settings try 2/3: HTTP 200 payload_present=True
climate_settings try 3/3: HTTP 200 payload_present=True
climate_status try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "basic",
"status": 0
}
climate_status try 2/3: HTTP 200 payload_present=True
climate_status try 3/3: HTTP 200 payload_present=True
Wake POST (NEW, not in v1):
refresh-climate-status: HTTP 200
ONE-GLOBAL-RS-10000: Request Completed Successfully
Post-refresh reads (1 try each, full payload dump):
climate_settings: HTTP 200 payload_present=True
sanitized_payload:
{
"temperature": null,
"temperatureUnit": null,
"minTemp": 18.0,
"maxTemp": 29.0,
"tempInterval": 1.0,
"acOperations": [],
"settingsOn": true
}
climate_status: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "basic",
"status": 0
}
</pre>
</details>
<h2 id="c-corolla">C &middot; Corolla HB/TS '23 (sciurius)</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0.post1.dev0+57ed082
Brand: toyota (code: T)
Found 1 vehicle(s).
=== Carolan (...071566) ===
Model: Corolla HB/TS - MC '23
features TRUE (10): ['climate_start_engine', 'dealer_appointment', 'hybrid_pulse', 'last_parked', 'privacy', 'remote_service', 'service_history', 'telemetry', 'vehicle_diagnostic', 'vehicle_status']
extended_capabilities TRUE (33): ['bonnet_status', 'buzzer_capable', 'climate_capable', 'climate_temperature_control_full', 'door_lock_unlock_capable', 'front_defogger', 'front_driver_door_lock_status', 'front_driver_door_open_status', 'front_driver_door_window_status', 'front_passenger_door_lock_status', 'front_passenger_door_open_status', 'front_passenger_door_window_status', 'fuel_level_available', 'fuel_range_available', 'guest_driver', 'hazard_capable', 'hybrid_pulse', 'last_parked_capable', 'light_status', 'rear_defogger', 'rear_driver_door_lock_status', 'rear_driver_door_open_status', 'rear_driver_door_window_status', 'rear_hatch_rear_window', 'rear_passenger_door_lock_status', 'rear_passenger_door_open_status', 'rear_passenger_door_window_status', 'smart_key_status', 'telemetry_capable', 'trunk_lock_unlock_capable', 'vehicle_diagnostic_capable', 'vehicle_finder', 'vehicle_status']
remote_service_capabilities TRUE (0): []
remoteDisplay: 7 (type=str, decoded=ACTIVATED)
features.remote_service: True
Predicted climate class: FULL_CLIMATE
-&gt; set target temp + on/off; POST /climate-control body: {command: 'engine-start', remoteHvac: {...}}; PUT /climate-settings updates ACOperations + temperature
Pre-refresh reads (3 tries each):
climate_settings try 1/3: EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_settings try 2/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_settings try 3/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_status try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "full",
"status": 0
}
climate_status try 2/3: HTTP 200 payload_present=True
climate_status try 3/3: HTTP 200 payload_present=True
Wake POST (NEW, not in v1):
refresh-climate-status: HTTP 200
ONE-GLOBAL-RS-10000: Request Completed Successfully
Post-refresh reads (1 try each, full payload dump):
climate_settings: EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_status: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "full",
"status": 0
}
</pre>
</details>
<h2 id="d-lexus-nx">D &middot; Lexus NX450h+ '21 (Paja-git) &mdash; PHEV</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0
Brand: lexus (code: L)
Found 1 vehicle(s).
=== NX450h+ (...002057) ===
Model: NX - NG '21
features TRUE (13): ['charging_station', 'dealer_appointment', 'ev_charge_station', 'home_charge', 'hybrid_pulse', 'last_parked', 'privacy', 'remote_service', 'service_history', 'smart_charging', 'telemetry', 'vehicle_diagnostic', 'vehicle_status']
extended_capabilities TRUE (47): ['bonnet_status', 'buzzer_capable', 'c_scheduling', 'charge_management', 'climate_capable', 'climate_temperature_control_full', 'door_lock_unlock_capable', 'econnect_vehicle_status_capable', 'equipped_with_alarm', 'ev_charge_stations_capable', 'front_defogger', 'front_driver_door_lock_status', 'front_driver_door_open_status', 'front_driver_door_window_status', 'front_driver_seat_heater', 'front_driver_seat_ventilation', 'front_passenger_door_lock_status', 'front_passenger_door_open_status', 'front_passenger_door_window_status', 'front_passenger_seat_heater', 'front_passenger_seat_ventilation', 'fuel_level_available', 'fuel_range_available', 'guest_driver', 'hazard_capable', 'horn_capable', 'hybrid_pulse', 'last_parked_capable', 'light_status', 'next_charge', 'power_windows_capable', 'rear_defogger', 'rear_driver_door_lock_status', 'rear_driver_door_open_status', 'rear_driver_door_window_status', 'rear_hatch_rear_window', 'rear_passenger_door_lock_status', 'rear_passenger_door_open_status', 'rear_passenger_door_window_status', 'remote_econnect_capable', 'smart_key_status', 'steering_heater', 'telemetry_capable', 'trunk_lock_unlock_capable', 'vehicle_diagnostic_capable', 'vehicle_finder', 'vehicle_status']
remote_service_capabilities TRUE (0): []
remoteDisplay: 9 (type=str, decoded=SUBSCRIPTION_EXPIRED_PRIMARY_USER)
features.remote_service: True
Predicted climate class: FULL_CLIMATE
-&gt; set target temp + on/off; POST /climate-control body: {command: 'engine-start', remoteHvac: {...}}; PUT /climate-settings updates ACOperations + temperature
Pre-refresh reads (3 tries each):
climate_settings try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"temperature": 22.0,
"temperatureUnit": "C",
"minTemp": 18.0,
"maxTemp": 29.0,
"tempInterval": 1.0,
"acOperations": [
{
"categoryName": "defrost",
"categoryDisplayName": "Defrost",
"available": true,
"acParameters": [
{"name": "frontDefrost", "displayName": "Front Defrost", "available": true, "enabled": true},
{"name": "rearDefrost", "displayName": "Rear Defrost", "available": true, "enabled": true}
]
},
{
"categoryName": "seatHeat",
"categoryDisplayName": "Seat Heat",
"available": true,
"acParameters": [
{"name": "frontDriver", "displayName": "Front, Driver", "available": true, "enabled": false},
{"name": "frontPassenger", "displayName": "Front, Passenger", "available": true, "enabled": false}
]
}
],
"settingsOn": false
}
climate_settings try 2/3: HTTP 200 payload_present=True
climate_settings try 3/3: HTTP 200 payload_present=True
climate_status try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "full",
"status": 0
}
climate_status try 2/3: HTTP 200 payload_present=True
climate_status try 3/3: HTTP 200 payload_present=True
Wake POST:
refresh-climate-status: HTTP 200
ONE-GLOBAL-RS-10000: Request Completed Successfully
Post-refresh reads (1 try each, full payload dump):
climate_settings: HTTP 200 payload_present=True (same as pre-refresh)
climate_status: HTTP 200 payload_present=True (type:full status:0)
(Defrost/seat-heat acOperations payload abbreviated; iconUrl strings stripped for brevity. Full attachment on issue #296.)
</pre>
</details>
<h2 id="e-bz4x">E &middot; BZ4X '22 (ArnstadFredrik) &mdash; BEV</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0
Brand: toyota (code: T)
Found 1 vehicle(s).
=== Solstad (...054783) ===
Model: D-SUV EV - NG '22
features TRUE (13): ['charging_station', 'dealer_appointment', 'electric_pulse', 'ev_charge_station', 'home_charge', 'last_parked', 'privacy', 'remote_service', 'service_history', 'smart_charging', 'telemetry', 'vehicle_diagnostic', 'vehicle_status']
extended_capabilities TRUE (45): ['bonnet_status', 'buzzer_capable', 'c_scheduling', 'charge_management', 'climate_capable', 'climate_temperature_control_full', 'door_lock_unlock_capable', 'econnect_vehicle_status_capable', 'electric_pulse', 'equipped_with_alarm', 'ev_charge_stations_capable', 'front_defogger', 'front_driver_door_lock_status', 'front_driver_door_open_status', 'front_driver_door_window_status', 'front_driver_seat_heater', 'front_passenger_door_lock_status', 'front_passenger_door_open_status', 'front_passenger_door_window_status', 'front_passenger_seat_heater', 'fuel_level_available', 'fuel_range_available', 'guest_driver', 'hazard_capable', 'horn_capable', 'last_parked_capable', 'light_status', 'power_windows_capable', 'rear_defogger', 'rear_driver_door_lock_status', 'rear_driver_door_open_status', 'rear_driver_door_window_status', 'rear_hatch_rear_window', 'rear_passenger_door_lock_status', 'rear_passenger_door_open_status', 'rear_passenger_door_window_status', 'remote_econnect_capable', 'smart_key_status', 'steering_heater', 'telemetry_capable', 'trunk_lock_unlock_capable', 'vehicle_diagnostic_capable', 'vehicle_finder', 'vehicle_status', 'weekly_charge']
remote_service_capabilities TRUE (0): []
remoteDisplay: 7 (type=str, decoded=ACTIVATED)
features.remote_service: True
Predicted climate class: FULL_CLIMATE
-&gt; set target temp + on/off; POST /climate-control body: {command: 'engine-start', remoteHvac: {...}}; PUT /climate-settings updates ACOperations + temperature
Pre-refresh reads (3 tries each):
climate_settings try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"temperature": 20.0,
"temperatureUnit": "C",
"minTemp": 18.0,
"maxTemp": 29.0,
"tempInterval": 1.0,
"acOperations": [
{
"categoryName": "defrost",
"categoryDisplayName": "Defrost",
"available": true,
"acParameters": [
{"name": "frontDefrost", "available": true, "enabled": true},
{"name": "rearDefrost", "available": true, "enabled": true}
]
}
],
"settingsOn": false
}
climate_settings try 2/3: HTTP 200 payload_present=True
climate_settings try 3/3: HTTP 200 payload_present=True
climate_status try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "full",
"status": 0
}
climate_status try 2/3: HTTP 200 payload_present=True
climate_status try 3/3: HTTP 200 payload_present=True
Wake POST:
refresh-climate-status: HTTP 200
ONE-GLOBAL-RS-10000: Request Completed Successfully
Post-refresh reads (1 try each, full payload dump):
climate_settings: HTTP 200 payload_present=True (same as pre-refresh)
climate_status: HTTP 200 payload_present=True (type:full status:0)
</pre>
</details>
<h2 id="f-yaris">F &middot; Yaris '25 (voyagers21)</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0.post2.dev0+42ca85b
Brand: toyota (code: T)
Found 1 vehicle(s).
=== Yaris Guy (...382230) ===
Model: YA202502 - MY '25
features TRUE (10): ['climate_start_engine', 'dealer_appointment', 'hybrid_pulse', 'last_parked', 'privacy', 'remote_service', 'service_history', 'telemetry', 'vehicle_diagnostic', 'vehicle_status']
extended_capabilities TRUE (33): ['bonnet_status', 'buzzer_capable', 'climate_capable', 'climate_temperature_control_full', 'door_lock_unlock_capable', 'front_defogger', 'front_driver_door_lock_status', 'front_driver_door_open_status', 'front_driver_door_window_status', 'front_passenger_door_lock_status', 'front_passenger_door_open_status', 'front_passenger_door_window_status', 'fuel_level_available', 'fuel_range_available', 'guest_driver', 'hazard_capable', 'hybrid_pulse', 'last_parked_capable', 'light_status', 'rear_defogger', 'rear_driver_door_lock_status', 'rear_driver_door_open_status', 'rear_driver_door_window_status', 'rear_hatch_rear_window', 'rear_passenger_door_lock_status', 'rear_passenger_door_open_status', 'rear_passenger_door_window_status', 'smart_key_status', 'telemetry_capable', 'trunk_lock_unlock_capable', 'vehicle_diagnostic_capable', 'vehicle_finder', 'vehicle_status']
remote_service_capabilities TRUE (0): []
remoteDisplay: 7 (type=str, decoded=ACTIVATED)
features.remote_service: True
Predicted climate class: FULL_CLIMATE
-&gt; set target temp + on/off; POST /climate-control body: {command: 'engine-start', remoteHvac: {...}}; PUT /climate-settings updates ACOperations + temperature
Pre-refresh reads (3 tries each):
climate_settings try 1/3: EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_settings try 2/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_settings try 3/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_status try 1/3: EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_status try 2/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_status try 3/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
Wake POST:
refresh-climate-status: HTTP 200
ONE-GLOBAL-RS-10000: Request Completed Successfully
Post-refresh reads (1 try each, full payload dump):
climate_settings: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
climate_status: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
</pre>
</details>
<h2 id="g-lexus-ux">G &middot; Lexus UX250h '19 (Nickduino)</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0
Brand: lexus (code: L)
Found 1 vehicle(s).
=== UX250h (...022091) ===
Model: UX - NG '19
features TRUE (7): ['dealer_appointment', 'hybrid_pulse', 'last_parked', 'privacy', 'service_history', 'telemetry', 'vehicle_diagnostic']
extended_capabilities TRUE (4): ['fuel_level_available', 'hybrid_pulse', 'telemetry_capable', 'vehicle_diagnostic_capable']
remote_service_capabilities TRUE (0): []
remoteDisplay: 9 (type=str, decoded=SUBSCRIPTION_EXPIRED_PRIMARY_USER)
features.remote_service: False
Predicted climate class: NO_CLIMATE
-&gt; no remote climate flags True; skip all climate calls
Pre-refresh reads (3 tries each):
climate_settings try 1/3: HTTP 200 payload_present=False
climate_settings try 2/3: HTTP 200 payload_present=False
climate_settings try 3/3: HTTP 200 payload_present=False
climate_status try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "basic",
"status": 0
}
climate_status try 2/3: HTTP 200 payload_present=True
climate_status try 3/3: HTTP 200 payload_present=True
Wake POST:
refresh-climate-status: HTTP None (EXCEPTION ToyotaApiError: Request Failed. 400, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-10068","description":"Remote Service feature is inactive for the vin","detailedDescription":"Remote Service feature is inactive for the vin"}]}}.)
Post-refresh reads (1 try each, full payload dump):
climate_settings: HTTP 200 payload_present=False
climate_status: HTTP 200 payload_present=True (type:basic status:0)
(Tester note: "Lexus Europe says there are no connected services for my VIN anyway. No range either, even though the element appears." So the EXPIRED state matches their account reality.)
</pre>
</details>
<h2 id="h-corolla-ng19">H &middot; Corolla TS NG'19 (jmalvarezf-lmes)</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0
Brand: toyota (code: T)
Found 1 vehicle(s).
=== MyCorolla (...009760) ===
Model: COROLLA TOURING SPORTS NG'19
features TRUE (6): ['dealer_appointment', 'hybrid_pulse', 'last_parked', 'privacy', 'service_history', 'telemetry']
extended_capabilities TRUE (2): ['hybrid_pulse', 'telemetry_capable']
remote_service_capabilities TRUE (0): []
remoteDisplay: 9 (type=str, decoded=SUBSCRIPTION_EXPIRED_PRIMARY_USER)
features.remote_service: False
Predicted climate class: NO_CLIMATE
-&gt; no remote climate flags True; skip all climate calls
Pre-refresh reads (3 tries each):
climate_settings try 1/3: HTTP 200 payload_present=False
climate_settings try 2/3: HTTP 200 payload_present=False
climate_settings try 3/3: HTTP 200 payload_present=False
climate_status try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"type": "basic",
"status": 0
}
climate_status try 2/3: HTTP 200 payload_present=True
climate_status try 3/3: HTTP 200 payload_present=True
Wake POST:
refresh-climate-status: HTTP None (EXCEPTION ToyotaApiError: Request Failed. 400, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-10068","description":"Remote Service feature is inactive for the vin","detailedDescription":"Remote Service feature is inactive for the vin"}]}}.)
Post-refresh reads (1 try each, full payload dump):
climate_settings: HTTP 200 payload_present=False
climate_status: HTTP 200 payload_present=True (type:basic status:0)
</pre>
</details>
<h2 id="i-corolla-buschi">I &middot; Corolla HB/TS '23 #2 (Buschi145)</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0
Brand: toyota (code: T)
Found 1 vehicle(s).
=== ===
Model: Corolla HB/TS - MC '23
features TRUE (11): ['climate_start_engine', 'dealer_appointment', 'digital_key', 'hybrid_pulse', 'last_parked', 'privacy', 'remote_service', 'service_history', 'telemetry', 'vehicle_diagnostic', 'vehicle_status']
extended_capabilities TRUE (33): ['bonnet_status', 'buzzer_capable', 'climate_capable', 'climate_temperature_control_full', 'door_lock_unlock_capable', 'front_defogger', 'front_driver_door_lock_status', 'front_driver_door_open_status', 'front_driver_door_window_status', 'front_passenger_door_lock_status', 'front_passenger_door_open_status', 'front_passenger_door_window_status', 'fuel_level_available', 'fuel_range_available', 'guest_driver', 'hazard_capable', 'hybrid_pulse', 'last_parked_capable', 'light_status', 'rear_defogger', 'rear_driver_door_lock_status', 'rear_driver_door_open_status', 'rear_driver_door_window_status', 'rear_hatch_rear_window', 'rear_passenger_door_lock_status', 'rear_passenger_door_open_status', 'rear_passenger_door_window_status', 'smart_key_status', 'telemetry_capable', 'trunk_lock_unlock_capable', 'vehicle_diagnostic_capable', 'vehicle_finder', 'vehicle_status']
remote_service_capabilities TRUE (0): []
remoteDisplay: 7 (type=str, decoded=ACTIVATED)
features.remote_service: True
Predicted climate class: FULL_CLIMATE
-&gt; set target temp + on/off; POST /climate-control body: {command: 'engine-start', remoteHvac: {...}}; PUT /climate-settings updates ACOperations + temperature
Pre-refresh reads (3 tries each):
climate_settings try 1/3: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
climate_settings try 2/3: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
climate_settings try 3/3: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
climate_status try 1/3: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
climate_status try 2/3: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
climate_status try 3/3: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
Wake POST:
refresh-climate-status: HTTP 200
ONE-GLOBAL-RS-10000: Request Completed Successfully
Post-refresh reads (1 try each, full payload dump):
climate_settings: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
climate_status: EXCEPTION ToyotaApiError: Request Failed. 500, RS-40000.
(Note: same model code as C/sciurius - Corolla HB/TS MC '23 - but Buschi145's blast radius extends to /climate-status too, matching F's Yaris '25 pattern. Strengthens "Toyota-side per-VIN data state" rather than model-wide gateway bug.)
</pre>
</details>
<h2 id="j-rav4-phev-eddie">J &middot; RAV4 PHEV '23 (Eddie84)</h2>
<details>
<summary>Full probe output</summary>
<pre>pytoyoda climate probe v2
pytoyoda version: 5.1.0
Brand: toyota (code: T)
Found 1 vehicle(s).
=== ... (...135249) ===
Model: Rav4 PHEV - MY '23
features TRUE (13): ['charging_station', 'dealer_appointment', 'ev_charge_station', 'home_charge', 'hybrid_pulse', 'last_parked', 'privacy', 'remote_service', 'service_history', 'smart_charging', 'telemetry', 'vehicle_diagnostic', 'vehicle_status']
extended_capabilities TRUE (36): ['bonnet_status', 'c_scheduling', 'charge_management', 'door_lock_unlock_capable', 'econnect_climate_capable', 'econnect_vehicle_status_capable', 'ev_charge_stations_capable', 'front_defogger', 'front_driver_door_lock_status', 'front_driver_door_open_status', 'front_driver_door_window_status', 'front_passenger_door_lock_status', 'front_passenger_door_open_status', 'front_passenger_door_window_status', 'fuel_level_available', 'fuel_range_available', 'guest_driver', 'hazard_capable', 'hybrid_pulse', 'last_parked_capable', 'light_status', 'next_charge', 'rear_defogger', 'rear_driver_door_lock_status', 'rear_driver_door_open_status', 'rear_driver_door_window_status', 'rear_hatch_rear_window', 'rear_passenger_door_lock_status', 'rear_passenger_door_open_status', 'rear_passenger_door_window_status', 'remote_econnect_capable', 'smart_key_status', 'telemetry_capable', 'vehicle_diagnostic_capable', 'vehicle_finder', 'vehicle_status']
remote_service_capabilities TRUE (0): []
remoteDisplay: 7 (type=str, decoded=ACTIVATED)
features.remote_service: True
Predicted climate class: ECONNECT
-&gt; Stellantis-derived variant; not enough data on shape; treat like FULL_CLIMATE pending more probe data
Pre-refresh reads (3 tries each):
climate_settings try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{
"temperature": 18.0,
"temperatureUnit": "C",
"minTemp": 18.0,
"maxTemp": 29.0,
"tempInterval": 1.0,
"acOperations": [
{
"categoryName": "defrost",
"categoryDisplayName": "Defrost",
"available": true,
"acParameters": [
{ "name": "frontDefrost", "available": true, "enabled": true },
{ "name": "rearDefrost", "available": true, "enabled": true }
]
}
],
"settingsOn": true
}
climate_settings try 2/3: HTTP 200 payload_present=True
climate_settings try 3/3: HTTP 200 payload_present=True
climate_status try 1/3: HTTP 200 payload_present=True
sanitized_payload:
{ "type": "full", "status": 0 }
climate_status try 2/3: HTTP 200 payload_present=True
climate_status try 3/3: HTTP 200 payload_present=True
Wake POST:
refresh-climate-status: HTTP 200
ONE-GLOBAL-RS-10000: Request Completed Successfully
Post-refresh reads (1 try each):
climate_settings: HTTP 200 payload_present=True (identical to pre)
climate_status: HTTP 200 payload_present=True (identical to pre)
(10th vehicle, 2nd PHEV. Confirms the eConnect-surface hypothesis - clean 200s on both endpoints, full eConnect cluster present, charging-management flags present. Note the acOperations under-reporting: only "defrost" returned even though the MyT app exposes more controls - same shape ArnstadFredrik flagged on his BZ4X. Notable: this is the only ECONNECT vehicle in the dataset that explicitly carries econnect_climate_capable in extended_capabilities; D and E carry the surface via the more general remote_econnect_capable + climate_capable + climate_temperature_control_full triplet instead.)
</pre>
</details>
<p class="meta">
Posted by <a href="https://github.com/nledenyi">nledenyi</a>.
Source data on <a href="https://github.com/pytoyoda/ha_toyota/issues/296">issue #296</a>.
</p>
</body>
</html>
@Paja-git

Paja-git commented Apr 29, 2026

Copy link
Copy Markdown

Could you add the brand to the script?

    brand_map = {"toyota": "T", "lexus": "L"}
    brand_code = brand_map.get(creds["Brand"], "T")  # default to Toyota

    client = MyT(username=creds["email"], password=creds["password"], brand=brand_code)

@nledenyi

Copy link
Copy Markdown
Author

incorporated 🙏

@unsnow-iac

unsnow-iac commented Jun 16, 2026

Copy link
Copy Markdown

`pytoyoda climate probe v2
pytoyoda version: 5.1.0
Brand: toyota (code: T)

Found 1 vehicle(s).

=== SB1===
Model: Corolla HB/TS - MC '23
features TRUE (10): ['climate_start_engine', 'dealer_appointment', 'hybrid_pulse', 'last_parked', 'privacy', 'remote_service', 'service_history', 'telemetry', 'vehicle_diagnostic', 'vehicle_status']
extended_capabilities TRUE (33): ['bonnet_status', 'buzzer_capable', 'climate_capable', 'climate_temperature_control_full', 'door_lock_unlock_capable', 'front_defogger', 'front_driver_door_lock_status', 'front_driver_door_open_status', 'front_driver_door_window_status', 'front_passenger_door_lock_status', 'front_passenger_door_open_status', 'front_passenger_door_window_status', 'fuel_level_available', 'fuel_range_available', 'guest_driver', 'hazard_capable', 'hybrid_pulse', 'last_parked_capable', 'light_status', 'rear_defogger', 'rear_driver_door_lock_status', 'rear_driver_door_open_status', 'rear_driver_door_window_status', 'rear_hatch_rear_window', 'rear_passenger_door_lock_status', 'rear_passenger_door_open_status', 'rear_passenger_door_window_status', 'smart_key_status', 'telemetry_capable', 'trunk_lock_unlock_capable', 'vehicle_diagnostic_capable', 'vehicle_finder', 'vehicle_status']
remote_service_capabilities TRUE (0): []
remoteDisplay: 7 (type=str, decoded=ACTIVATED)
features.remote_service: True
Predicted climate class: FULL_CLIMATE
-> set target temp + on/off; POST /climate-control body: {command: 'engine-start', remoteHvac: {...}}; PUT /climate-settings updates ACOperations + temperature

Pre-refresh reads (3 tries each):
climate_settings try 1/3: EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_settings try 2/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_settings try 3/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_status try 1/3: EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_status try 2/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_status try 3/3: HTTP None EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.

Wake POST (NEW, not in v1):
refresh-climate-status: HTTP 200
ONE-GLOBAL-RS-10000: Request Completed Successfully

Post-refresh reads (1 try each, full payload dump):
climate_settings: EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.
climate_status: EXCEPTION ToyotaApiError: Request Failed. 500, {"status":{"messages":[{"responseCode":"ONE-GLOBAL-RS-40000","description":"Command execution interrupted. Try again.","detailedDescription":"Request Processing Failed"}]}}.`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment