The cabin's temperature, and the one it is heading for

The climate cards landed with the endpoint migration, but only as two more
folded dumps of key/value pairs. What a driver opens that tab for in January is
one number, and it was three taps down inside a card called Climate.

So currentTemperature and targetTemperature join the headline readings, beside
the pair of electric ranges and for the same stated reason: neither figure
answers the question on its own. A cabin at 12° means nothing until you know it
is climbing towards 21°, and the gap between them is how long to leave the
scraper in the boot. Being derived from headlineMetricSpecs, both are arrangeable
the moment they exist — a car's saved order of readings can name them without
anything else being told they are there, and a test now says so rather than
leaving it to be noticed when a PATCH starts rejecting a key.

The unit is fixed at Celsius, because Toyota Connected is the European service
and there is no imperial reading to convert from. That is a default and not a
claim: a payload that names its own unit is still believed over it, the way
every other reading here works, so a service that one day reports Fahrenheit is
labelled Fahrenheit rather than relabelled into a wrong Celsius.

The two apps needed the two labels in three languages each and nothing else.
That is the shape working: a section is an id the app localizes and a reading is
a key it localizes, so a card added on the server arrives in both clients
already folded, already arrangeable, already translated. The one thing the Web
App did need was a corrected comment — the note explaining why cards fold still
said Toyota reports eight sections, and it is the argument for folding them, so
it should count the ten there now are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-08-29 19:45:58 +02:00
co-authored by Claude Opus 5
parent 22a22ec43a
commit fd75833707
9 changed files with 63 additions and 1 deletions
@@ -648,6 +648,14 @@ var headlineMetricSpecs = []metricSpec{
// are different numbers and the gap between them is the point — a driver // are different numbers and the gap between them is the point — a driver
// deciding whether to run the A/C wants to see both. // deciding whether to run the A/C wants to see both.
{key: "evRangeWithAc", keys: []string{"evRangeWithAc"}, unit: "km", distance: true}, {key: "evRangeWithAc", keys: []string{"evRangeWithAc"}, unit: "km", distance: true},
// Cabin temperature and the preset it is climbing towards, out of the climate
// section. Two readings for the same reason the two ranges are two: the gap
// between them is the answer to the question the driver actually has, which is
// whether the car is warm yet. Unit is fixed rather than converted — Toyota
// Connected is a European service reporting Celsius — but a payload that names
// its own unit still wins, as it does for every other reading here.
{key: "cabinTemperature", keys: []string{"currentTemperature", "cabinTemperature", "insideTemperature"}, unit: "°C"},
{key: "targetTemperature", keys: []string{"targetTemperature"}, unit: "°C"},
} }
// roundForDisplay trims a reading to the precision it actually has. // roundForDisplay trims a reading to the precision it actually has.
@@ -194,6 +194,48 @@ func TestHeadlineMetrics(t *testing.T) {
} }
} }
// The climate section contributes two readings, and the pair is the point: a
// cabin at 12° heading for 21° is a car still warming up, which neither figure
// says on its own.
func TestHeadlineMetricsClimateTemperatures(t *testing.T) {
trees := []any{
decode(t, `{"payload": {"status": "on", "currentTemperature": 12.5,
"targetTemperature": 21, "duration": 10}}`),
}
got := map[string]providerMetric{}
for _, m := range headlineMetrics(trees) {
got[m.Key] = m
}
if got["cabinTemperature"].Value != "12.5" || got["cabinTemperature"].Unit != "°C" {
t.Errorf("cabinTemperature = %+v, want 12.5 °C", got["cabinTemperature"])
}
if got["targetTemperature"].Value != "21" || got["targetTemperature"].Unit != "°C" {
t.Errorf("targetTemperature = %+v, want 21 °C", got["targetTemperature"])
}
// A payload that names its own unit is believed over the fixed default, so a
// service reporting Fahrenheit is not relabelled into a wrong Celsius.
trees = []any{decode(t, `{"payload": {"currentTemperature": {"value": 68, "unit": "°F"}}}`)}
got = map[string]providerMetric{}
for _, m := range headlineMetrics(trees) {
got[m.Key] = m
}
if got["cabinTemperature"].Value != "68" || got["cabinTemperature"].Unit != "°F" {
t.Errorf("cabinTemperature = %+v, want 68 °F", got["cabinTemperature"])
}
}
// Every headline reading must be nameable in a car's saved arrangement, or the
// tab would show a reading the user cannot move.
func TestArrangeableCarMetricsCoverHeadlines(t *testing.T) {
for _, spec := range headlineMetricSpecs {
if !arrangeableCarMetrics[spec.key] {
t.Errorf("reading %q is shown but cannot be arranged", spec.key)
}
}
}
// A reading converted out of miles must not claim to know the range to the // A reading converted out of miles must not claim to know the range to the
// metre. 62 mi is 99.779136 km exactly; the tab shows 99.8, the way the same // metre. 62 mi is 99.779136 km exactly; the tab shows 99.8, the way the same
// figure reported in km already would read. // figure reported in km already would read.
+2
View File
@@ -452,6 +452,8 @@
"batteryLevel": "Batteri", "batteryLevel": "Batteri",
"evRange": "Elektrisk rækkevidde (uden aircon)", "evRange": "Elektrisk rækkevidde (uden aircon)",
"evRangeWithAc": "Elektrisk rækkevidde (med aircon)", "evRangeWithAc": "Elektrisk rækkevidde (med aircon)",
"cabinTemperature": "Kabinetemperatur",
"targetTemperature": "Måltemperatur",
"chargingStatus": "Opladning", "chargingStatus": "Opladning",
"location": "Position" "location": "Position"
} }
+2
View File
@@ -452,6 +452,8 @@
"batteryLevel": "Battery", "batteryLevel": "Battery",
"evRange": "Electric range (A/C off)", "evRange": "Electric range (A/C off)",
"evRangeWithAc": "Electric range (A/C on)", "evRangeWithAc": "Electric range (A/C on)",
"cabinTemperature": "Cabin temperature",
"targetTemperature": "Target temperature",
"chargingStatus": "Charging", "chargingStatus": "Charging",
"location": "Position" "location": "Position"
} }
+2
View File
@@ -456,6 +456,8 @@
"batteryLevel": "Akumulator", "batteryLevel": "Akumulator",
"evRange": "Zasięg elektryczny (bez klimatyzacji)", "evRange": "Zasięg elektryczny (bez klimatyzacji)",
"evRangeWithAc": "Zasięg elektryczny (z klimatyzacją)", "evRangeWithAc": "Zasięg elektryczny (z klimatyzacją)",
"cabinTemperature": "Temperatura w kabinie",
"targetTemperature": "Temperatura docelowa",
"chargingStatus": "Ładowanie", "chargingStatus": "Ładowanie",
"location": "Pozycja" "location": "Pozycja"
} }
+1 -1
View File
@@ -147,7 +147,7 @@ async function applyOdometer() {
// --- Collapsing the cards --- // --- Collapsing the cards ---
// //
// Every card below the headline readings folds away, so a long provider dump // Every card below the headline readings folds away, so a long provider dump
// (Toyota reports eight sections) can be trimmed to the two or three worth // (Toyota reports ten sections) can be trimmed to the two or three worth
// watching. Which ones are folded is remembered in localStorage rather than on // watching. Which ones are folded is remembered in localStorage rather than on
// the profile: it is a per-device reading habit, not an account setting, and it // the profile: it is a per-device reading habit, not an account setting, and it
// should survive leaving the tab without a round trip. Keyed by section id, so // should survive leaving the tab without a round trip. Keyed by section id, so
+2
View File
@@ -433,6 +433,8 @@
"batteryLevel": "Batteri", "batteryLevel": "Batteri",
"evRange": "Elektrisk rækkevidde (uden aircon)", "evRange": "Elektrisk rækkevidde (uden aircon)",
"evRangeWithAc": "Elektrisk rækkevidde (med aircon)", "evRangeWithAc": "Elektrisk rækkevidde (med aircon)",
"cabinTemperature": "Kabinetemperatur",
"targetTemperature": "Måltemperatur",
"chargingStatus": "Opladning", "chargingStatus": "Opladning",
"location": "Position" "location": "Position"
} }
+2
View File
@@ -432,6 +432,8 @@
"batteryLevel": "Battery", "batteryLevel": "Battery",
"evRange": "Electric range (A/C off)", "evRange": "Electric range (A/C off)",
"evRangeWithAc": "Electric range (A/C on)", "evRangeWithAc": "Electric range (A/C on)",
"cabinTemperature": "Cabin temperature",
"targetTemperature": "Target temperature",
"chargingStatus": "Charging", "chargingStatus": "Charging",
"location": "Position" "location": "Position"
} }
+2
View File
@@ -437,6 +437,8 @@
"batteryLevel": "Akumulator", "batteryLevel": "Akumulator",
"evRange": "Zasięg elektryczny (bez klimatyzacji)", "evRange": "Zasięg elektryczny (bez klimatyzacji)",
"evRangeWithAc": "Zasięg elektryczny (z klimatyzacją)", "evRangeWithAc": "Zasięg elektryczny (z klimatyzacją)",
"cabinTemperature": "Temperatura w kabinie",
"targetTemperature": "Temperatura docelowa",
"chargingStatus": "Ładowanie", "chargingStatus": "Ładowanie",
"location": "Pozycja" "location": "Pozycja"
} }