From fd75833707903591f1d93235f848f8f91c26f325 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:45:58 +0200 Subject: [PATCH] The cabin's temperature, and the one it is heading for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- API Server/internal/api/vehicleproviders.go | 8 ++++ .../internal/api/vehicleproviders_test.go | 42 +++++++++++++++++++ Phone App/assets/i18n/da.json | 2 + Phone App/assets/i18n/en.json | 2 + Phone App/assets/i18n/pl.json | 2 + Web App/web/src/components/ProviderPanel.vue | 2 +- Web App/web/src/i18n/da.json | 2 + Web App/web/src/i18n/en.json | 2 + Web App/web/src/i18n/pl.json | 2 + 9 files changed, 63 insertions(+), 1 deletion(-) diff --git a/API Server/internal/api/vehicleproviders.go b/API Server/internal/api/vehicleproviders.go index 893f533..c22a914 100644 --- a/API Server/internal/api/vehicleproviders.go +++ b/API Server/internal/api/vehicleproviders.go @@ -648,6 +648,14 @@ var headlineMetricSpecs = []metricSpec{ // are different numbers and the gap between them is the point — a driver // deciding whether to run the A/C wants to see both. {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. diff --git a/API Server/internal/api/vehicleproviders_test.go b/API Server/internal/api/vehicleproviders_test.go index d5de038..c3b8327 100644 --- a/API Server/internal/api/vehicleproviders_test.go +++ b/API Server/internal/api/vehicleproviders_test.go @@ -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 // metre. 62 mi is 99.779136 km exactly; the tab shows 99.8, the way the same // figure reported in km already would read. diff --git a/Phone App/assets/i18n/da.json b/Phone App/assets/i18n/da.json index f77a170..61f2fa6 100644 --- a/Phone App/assets/i18n/da.json +++ b/Phone App/assets/i18n/da.json @@ -452,6 +452,8 @@ "batteryLevel": "Batteri", "evRange": "Elektrisk rækkevidde (uden aircon)", "evRangeWithAc": "Elektrisk rækkevidde (med aircon)", + "cabinTemperature": "Kabinetemperatur", + "targetTemperature": "Måltemperatur", "chargingStatus": "Opladning", "location": "Position" } diff --git a/Phone App/assets/i18n/en.json b/Phone App/assets/i18n/en.json index e67b3d3..8377334 100644 --- a/Phone App/assets/i18n/en.json +++ b/Phone App/assets/i18n/en.json @@ -452,6 +452,8 @@ "batteryLevel": "Battery", "evRange": "Electric range (A/C off)", "evRangeWithAc": "Electric range (A/C on)", + "cabinTemperature": "Cabin temperature", + "targetTemperature": "Target temperature", "chargingStatus": "Charging", "location": "Position" } diff --git a/Phone App/assets/i18n/pl.json b/Phone App/assets/i18n/pl.json index b617f20..822c126 100644 --- a/Phone App/assets/i18n/pl.json +++ b/Phone App/assets/i18n/pl.json @@ -456,6 +456,8 @@ "batteryLevel": "Akumulator", "evRange": "Zasięg elektryczny (bez klimatyzacji)", "evRangeWithAc": "Zasięg elektryczny (z klimatyzacją)", + "cabinTemperature": "Temperatura w kabinie", + "targetTemperature": "Temperatura docelowa", "chargingStatus": "Ładowanie", "location": "Pozycja" } diff --git a/Web App/web/src/components/ProviderPanel.vue b/Web App/web/src/components/ProviderPanel.vue index 99ba0f0..fc48c2a 100644 --- a/Web App/web/src/components/ProviderPanel.vue +++ b/Web App/web/src/components/ProviderPanel.vue @@ -147,7 +147,7 @@ async function applyOdometer() { // --- Collapsing the cards --- // // 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 // 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 diff --git a/Web App/web/src/i18n/da.json b/Web App/web/src/i18n/da.json index d8a809a..11d41d5 100644 --- a/Web App/web/src/i18n/da.json +++ b/Web App/web/src/i18n/da.json @@ -433,6 +433,8 @@ "batteryLevel": "Batteri", "evRange": "Elektrisk rækkevidde (uden aircon)", "evRangeWithAc": "Elektrisk rækkevidde (med aircon)", + "cabinTemperature": "Kabinetemperatur", + "targetTemperature": "Måltemperatur", "chargingStatus": "Opladning", "location": "Position" } diff --git a/Web App/web/src/i18n/en.json b/Web App/web/src/i18n/en.json index 823d265..48d31e8 100644 --- a/Web App/web/src/i18n/en.json +++ b/Web App/web/src/i18n/en.json @@ -432,6 +432,8 @@ "batteryLevel": "Battery", "evRange": "Electric range (A/C off)", "evRangeWithAc": "Electric range (A/C on)", + "cabinTemperature": "Cabin temperature", + "targetTemperature": "Target temperature", "chargingStatus": "Charging", "location": "Position" } diff --git a/Web App/web/src/i18n/pl.json b/Web App/web/src/i18n/pl.json index 5df9625..a29c999 100644 --- a/Web App/web/src/i18n/pl.json +++ b/Web App/web/src/i18n/pl.json @@ -437,6 +437,8 @@ "batteryLevel": "Akumulator", "evRange": "Zasięg elektryczny (bez klimatyzacji)", "evRangeWithAc": "Zasięg elektryczny (z klimatyzacją)", + "cabinTemperature": "Temperatura w kabinie", + "targetTemperature": "Temperatura docelowa", "chargingStatus": "Ładowanie", "location": "Pozycja" }