From d0ea619d09506c7f085e37128c3f627853948ca9 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Fri, 25 Sep 2026 06:45:26 +0200 Subject: [PATCH] fix(easee_cloud): stop reporting an old change time for steady power Easee records TotalPower (observation 120) only when it changes. The driver passed that timestamp as power_observed_at, so a car charging at a steady current reported power that looked older and older although every 5 s poll confirmed it. FTW treats a non-zero reading older than power_max_age_s (180 s) as stale; on the owner's box it stood the charger down about every three minutes all night (srcfl/ftw#1417). An unchanged value now omits power_observed_at, so the host stamps the reading when it arrives. A new value keeps Easee's source time, and an offline charger still emits no sample. No clock arithmetic: the 32-bit Lua harness cannot hold epoch milliseconds exactly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MuerPFZFG88kgu8sWVHeq7 Signed-off-by: Fredrik Ahlgren --- CHANGELOG.md | 4 +++ SUPPORT_STATUS.md | 4 +-- devices.yaml | 2 +- drivers/lua/easee_cloud.lua | 20 ++++++++--- .../test_easee_cloud_power_time.lua | 36 +++++++++++++++++++ drivers/tests/test_easee_cloud_power_time.py | 12 +++++++ index.yaml | 6 ++-- manifests/easee_cloud.yaml | 6 ++-- support-status.json | 2 +- 9 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 drivers/tests/lua_harness/test_easee_cloud_power_time.lua create mode 100644 drivers/tests/test_easee_cloud_power_time.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d4d4a..a5c5306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## easee_cloud 1.3.3 + +Omit `power_observed_at` when the power is unchanged since the last poll, so the host stamps the reading on arrival. Easee records TotalPower only when it changes, so a steady charge kept an old timestamp; FTW took it as a stale charger after three minutes and stopped the car every few minutes all night ([srcfl/ftw#1417](https://github.com/srcfl/ftw/pull/1417)). A new value still carries Easee's source time, and an offline charger (op_mode 0) still emits no sample. + ## easee_cloud 1.3.2 Restore the current session after restart even when Easee fills sessionEnd during a pause. Keep the ID when the car stops drawing; revoke it on a real unplug. Retain each measurement's source time and omit an old no-current reason while fresh charging power flows. Cloud offline state no longer reports a cable unplug. diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index 2e32c18..7a239ed 100644 --- a/SUPPORT_STATUS.md +++ b/SUPPORT_STATUS.md @@ -46,8 +46,8 @@ Catalog source is not proof that a target can install or run a driver. | deye | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | easee | 1.0.4 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | easee | 1.0.4 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| easee_cloud | 1.3.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| easee_cloud | 1.3.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| easee_cloud | 1.3.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| easee_cloud | 1.3.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | esphome-dsmr | 1.0.3 | ftw-core | not_assessed | 1.0.2 | — | not_recorded | — | not_assessed | no | | esphome-dsmr | 1.0.3 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | esphome_dsmr | 1.0.3 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | diff --git a/devices.yaml b/devices.yaml index d019acc..c379581 100644 --- a/devices.yaml +++ b/devices.yaml @@ -349,7 +349,7 @@ manufacturers: protocols: - protocol: http driver: "easee_cloud" - version: "1.3.2" + version: "1.3.3" ders: [ev] control: true firmware_versions: "" diff --git a/drivers/lua/easee_cloud.lua b/drivers/lua/easee_cloud.lua index 3bc019a..72b3429 100644 --- a/drivers/lua/easee_cloud.lua +++ b/drivers/lua/easee_cloud.lua @@ -25,7 +25,7 @@ DRIVER = { id = "easee-cloud", name = "Easee Cloud", manufacturer = "Easee", - version = "1.3.2", + version = "1.3.3", protocols = { "http" }, capabilities = { "ev" }, description = "Easee Home/Charge via Cloud REST API. No local protocol needed.", @@ -282,6 +282,7 @@ local OBS_SESSION_START = 223 local OBS_IDS = "48,96,103,109,120,121,124,183,194,223" +local last_power_observed_at = nil local validated_session_id = nil local observed_session_id = nil local departed_session_id = nil @@ -599,6 +600,19 @@ function driver_poll() -- responsive even when no car is plugged in. local is_online = (op_mode ~= 0) + -- Easee records an observation only when its value changes, so a steady + -- charge keeps an old TotalPower timestamp; on hardware it stayed + -- unchanged for over five minutes. A new value carries its source time. + -- An unchanged one was confirmed by this poll of a charger the cloud + -- still hears from (get_observations rejects op_mode 0), so omit the + -- source time and let the host stamp the reading when it arrives. + local power_observed_at = timestamps[OBS_TOTAL_POWER] + if power_observed_at ~= nil and power_observed_at == last_power_observed_at then + power_observed_at = nil + else + last_power_observed_at = power_observed_at + end + local reason_code = obs[OBS_REASON_NO_CUR] -- ReasonForNoCurrent describes a blocked offer. An older reason is not -- a current fault while the charger reports both charging and power. @@ -673,9 +687,7 @@ function driver_poll() charging = charging, request_active = request_active, session_wh = session_wh, - power_observed_at = timestamps[OBS_TOTAL_POWER], - -- Two-minute source updates were observed on hardware. One minute - -- of margin bounds the power estimate without refreshing its time. + power_observed_at = power_observed_at, power_max_age_s = 180, energy_observed_at = timestamps[OBS_SESSION_ENERGY], state_observed_at = timestamps[OBS_OP_MODE], diff --git a/drivers/tests/lua_harness/test_easee_cloud_power_time.lua b/drivers/tests/lua_harness/test_easee_cloud_power_time.lua new file mode 100644 index 0000000..2ab256d --- /dev/null +++ b/drivers/tests/lua_harness/test_easee_cloud_power_time.lua @@ -0,0 +1,36 @@ +dofile("drivers/tests/lua_harness/host_mock.lua") +-- Easee records TotalPower only when it changes. A steady charge must not +-- keep reporting that old change time as when the power was observed. +local driver = "drivers/lua/easee_cloud.lua" +host.reset() +host._http_responses["/accounts/login"] = '{"accessToken":"test","expiresIn":3600}' +host._http_responses["/config"] = '{}' +host._http_responses["/sessions/ongoing"] = '{}' +dofile(driver) +driver_init({email="test@example.invalid",password="test",serial="TEST123"}) + +local function poll(mode, power_kw, power_time) + host._http_responses["/observations?ids="] = host.json_encode({ + {id=109,value=mode,timestamp="2026-09-25T00:21:00Z"}, + {id=120,value=power_kw,timestamp=power_time}, + {id=121,value=12.5,timestamp=power_time}, + }) + driver_poll() + local rows = host._emitted.ev + assert(rows and #rows > 0, "no EV sample") + return rows[#rows], #rows +end + +local first = poll(3, 4.92, "2026-09-25T00:21:20Z") +assert(first.power_observed_at == "2026-09-25T00:21:20Z", "a new value lost its source time") +local steady = poll(3, 4.92, "2026-09-25T00:21:20Z") +assert(steady.power_observed_at == nil, + "an unchanged value kept its old change time: " .. tostring(steady.power_observed_at)) +local changed = poll(3, 6.30, "2026-09-25T00:29:20Z") +assert(changed.power_observed_at == "2026-09-25T00:29:20Z", "a changed value lost its source time") + +-- An offline charger (op_mode 0) emits no sample, so nothing looks fresh. +local _, before = poll(3, 6.30, "2026-09-25T00:29:20Z") +local _, after = poll(0, 6.30, "2026-09-25T00:29:20Z") +assert(after == before, "an offline charger emitted a sample") +print("Easee power time: passed") diff --git a/drivers/tests/test_easee_cloud_power_time.py b/drivers/tests/test_easee_cloud_power_time.py new file mode 100644 index 0000000..300f455 --- /dev/null +++ b/drivers/tests/test_easee_cloud_power_time.py @@ -0,0 +1,12 @@ +"""A steady Easee charge must not look like a stale power reading.""" +from pathlib import Path +import subprocess + + +def test_easee_cloud_reports_poll_time_for_steady_power(): + root = Path(__file__).resolve().parents[2] + result = subprocess.run( + [str(root / "lua55"), "drivers/tests/lua_harness/test_easee_cloud_power_time.lua"], + cwd=root, text=True, capture_output=True, check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/index.yaml b/index.yaml index 6978808..bd94a91 100644 --- a/index.yaml +++ b/index.yaml @@ -193,15 +193,15 @@ drivers: size_bytes: 4054 sha256: "4a2cd1efb4a5583468ce897ebd88a000e348917295c40210e86ecf834c0ba543" - name: "easee_cloud" - version: "1.3.2" + version: "1.3.3" tier: core protocol: http connectivity: cloud setup: [vendor_portal] ders: [ev] control: true - size_bytes: 38746 - sha256: "c0b1988b8aed68cf4c67034e0247f23133c33f4cba491e98d92131ce2332d578" + size_bytes: 39308 + sha256: "1ae0b702197553df0075a649a161ecce57b2a3afe69322afa78edd585f112232" - name: "esphome-dsmr" version: "1.0.3" tier: community diff --git a/manifests/easee_cloud.yaml b/manifests/easee_cloud.yaml index 8f5c10e..7a8ab9f 100644 --- a/manifests/easee_cloud.yaml +++ b/manifests/easee_cloud.yaml @@ -1,5 +1,5 @@ name: "easee_cloud" -version: "1.3.2" +version: "1.3.3" tier: core author: "Sourceful Labs AB" protocol: http @@ -16,9 +16,9 @@ tested_devices: notes: "Easee Home/Charge via Cloud REST API. No local protocol needed." min_driver_version: "1.0.1" min_host_version: "2.0.0" -size_bytes: 38746 +size_bytes: 39308 dkb_id: "easee_cloud" -sha256: "c0b1988b8aed68cf4c67034e0247f23133c33f4cba491e98d92131ce2332d578" +sha256: "1ae0b702197553df0075a649a161ecce57b2a3afe69322afa78edd585f112232" signature: "" bytecode_sha256: "" diff --git a/support-status.json b/support-status.json index 6a1d053..7a93e1a 100644 --- a/support-status.json +++ b/support-status.json @@ -590,7 +590,7 @@ }, { "catalog_source": true, - "catalog_version": "1.3.2", + "catalog_version": "1.3.3", "driver_id": "easee_cloud", "package_id": null, "targets": {