From 1f970eacc72043ec2c228f152c052775942b2555 Mon Sep 17 00:00:00 2001 From: Erik <758402+erikarenhill@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:58:24 +0200 Subject: [PATCH 1/3] fix(easee_cloud): wait before resuming a mid-session phase flip Easee applies a new phaseMode only when the session restarts. The driver paused, wrote phaseMode and resumed inside one ev_set_current, about 0.3 s apart. The cloud queues commands, so the resume reached the charger before the contactor opened and the session kept its phase count. On an Easee Home with a Tesla, phaseMode=3 was written mid-session and the car drew 15 A on one phase for hours until the owner paused and resumed in the Easee app. Hold the auto-resume for 15 s after a flip pause. The host's following ev_set_current ticks resume once the delay has passed. An explicit ev_pause or ev_resume clears the delay. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01766Rey7C5yDqY8qnnc92SA Signed-off-by: Erik <758402+erikarenhill@users.noreply.github.com> Signed-off-by: Fredrik Ahlgren --- CHANGELOG.md | 11 ++++ SUPPORT_STATUS.md | 4 +- devices.yaml | 2 +- drivers/lua/easee_cloud.lua | 37 ++++++++++--- .../test_easee_cloud_phase_flip.lua | 52 +++++++++++++++++++ drivers/tests/test_easee_cloud_phase_flip.py | 12 +++++ index.yaml | 6 +-- manifests/easee_cloud.yaml | 6 +-- support-status.json | 2 +- 9 files changed, 114 insertions(+), 18 deletions(-) create mode 100644 drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua create mode 100644 drivers/tests/test_easee_cloud_phase_flip.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b358dd3..e3bdb1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## easee_cloud 1.3.7 + +Wait 15 s after the pause before resuming a mid-session 1Φ↔3Φ flip. Easee +applies a new `phaseMode` only when the session restarts, and the cloud queues +commands, so the resume sent ~0.3 s after the pause in the same command +reached the charger before the contactor opened. On an Easee Home with a +Tesla, `phaseMode=3` was written mid-session and the car drew 15 A on one +phase for hours until the owner paused and resumed in the Easee app. The +following `ev_set_current` ticks now resume once the delay has passed; an +explicit `ev_pause` or `ev_resume` clears it. Not yet verified on hardware. + ## heishamon 0.8.0 Read `hp_power_w` from `main/Heat_Power_Consumption` (TOP16), with a diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index 40e329d..421b86e 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 | | easee | 1.0.4 | ftw-core | not_assessed | — | not_recorded | not_assessed | | easee | 1.0.4 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | -| easee_cloud | 1.3.6 | ftw-core | not_assessed | — | not_recorded | not_assessed | -| easee_cloud | 1.3.6 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | +| easee_cloud | 1.3.7 | ftw-core | not_assessed | — | not_recorded | not_assessed | +| easee_cloud | 1.3.7 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | | esphome_dsmr | 1.0.7 | ftw-core | not_assessed | — | not_recorded | not_assessed | | esphome_dsmr | 1.0.7 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | | etrel | 1.0.3 | ftw-core | not_assessed | — | not_recorded | not_assessed | diff --git a/devices.yaml b/devices.yaml index 592c11c..f9337aa 100644 --- a/devices.yaml +++ b/devices.yaml @@ -338,7 +338,7 @@ manufacturers: protocols: - protocol: http driver: "easee_cloud" - version: "1.3.6" + version: "1.3.7" ders: [ev] control: true firmware_versions: "" diff --git a/drivers/lua/easee_cloud.lua b/drivers/lua/easee_cloud.lua index f8a04fe..7b5d8c8 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.6", + version = "1.3.7", protocols = { "http" }, capabilities = { "ev" }, description = "Easee Home/Charge via Cloud REST API. No local protocol needed.", @@ -72,6 +72,17 @@ local last_amps_set = nil -- "easee_a=6, easee_chg=false, ev_w=0, reason=100/52" stuck states. local paused_state = false +-- A mid-session phase flip only takes effect when the charger restarts +-- the session. The cloud queues commands, so a resume that follows the +-- pause within the same driver_command (~0.3 s) reached the charger before +-- the contactor opened; the session went on at the old phase count. +-- Field case, Easee Home + Tesla: phaseMode=3 written mid-session, the car +-- drew 15 A on one phase for hours until a pause/resume in the Easee app. +-- So hold the auto-resume until the pause has had time to land. Later +-- ev_set_current ticks (every ~5 s from the host) resume once it expires. +local PHASE_FLIP_RESUME_DELAY_MS = 15000 +local phase_flip_resume_at_ms = nil + -- command_stalled_since_ms tracks when we last wrote a non-zero amps -- offer that did NOT translate into actual charging. Used to surface -- a derived diagnostic for the controller / UI so a stuck Easee @@ -791,11 +802,17 @@ function driver_command(action, power_w, cmd) return ok elseif action == "ev_pause" then local ok = post_command("/commands/pause_charging") - if ok then paused_state = true end + if ok then + paused_state = true + phase_flip_resume_at_ms = nil + end return ok elseif action == "ev_resume" then local ok = post_command("/commands/resume_charging") - if ok then paused_state = false end + if ok then + paused_state = false + phase_flip_resume_at_ms = nil + end return ok elseif action == "ev_set_current" then -- Driver-level phase decision: read the operator's preferences @@ -822,13 +839,15 @@ function driver_command(action, power_w, cmd) -- latched when a session (re)starts. So on a real mid-session flip -- pause first; the phaseMode write reconfigures, and the auto- -- resume below (amps > 0 && paused_state) re-closes the contactor - -- on the new phase count. Operator-confirmed: a manual - -- pause+resume was the only thing that flipped 1Φ→3Φ. Skip on the - -- first command of a session (last_sent_phases == nil) — there's no - -- live contactor to recycle. 2026-05-30. + -- on the new phase count once PHASE_FLIP_RESUME_DELAY_MS has + -- passed. Operator-confirmed: a manual pause+resume was the only + -- thing that flipped 1Φ→3Φ. Skip on the first command of a + -- session (last_sent_phases == nil) — there's no live contactor + -- to recycle. 2026-05-30. if last_sent_phases ~= nil then if post_command("/commands/pause_charging") then paused_state = true + phase_flip_resume_at_ms = now_ms + PHASE_FLIP_RESUME_DELAY_MS host.log("info", "Easee: pause to flip phaseMode " .. tostring(last_sent_phases) .. "→" .. tostring(requested_phases)) end @@ -877,9 +896,11 @@ function driver_command(action, power_w, cmd) -- the new offer is > 0. Idempotent on the Easee side; -- a failure here doesn't fail the command (the offer -- itself succeeded, controller will retry next tick). - if amps > 0 and paused_state then + local flip_settling = phase_flip_resume_at_ms ~= nil and now_ms < phase_flip_resume_at_ms + if amps > 0 and paused_state and not flip_settling then if post_command("/commands/resume_charging") then paused_state = false + phase_flip_resume_at_ms = nil host.log("info", "Easee: auto-resumed after pause (offer=" .. tostring(amps) .. " A)") end diff --git a/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua b/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua new file mode 100644 index 0000000..5c40d1a --- /dev/null +++ b/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua @@ -0,0 +1,52 @@ +dofile("drivers/tests/lua_harness/host_mock.lua") +-- A mid-session phase flip only takes effect when the charger restarts the +-- session. Resuming in the same command as the pause reached the charger +-- before the contactor opened, so the car stayed on one phase. +local driver = "drivers/lua/easee_cloud.lua" +host.reset() +host._millis_step = 0 +host._http_responses["/accounts/login"] = '{"accessToken":"test","expiresIn":3600}' +host._http_responses["/config"] = '{}' +host._http_responses["/sessions/ongoing"] = '{}' +host._http_responses["/commands/"] = '{}' +host._http_responses["/settings"] = '{}' +dofile(driver) +driver_init({email="test@example.invalid",password="test",serial="TEST123"}) + +local function posts(path) + local n = 0 + for _, call in ipairs(host._calls) do + if call.func == "http_post" and string.find(call.args[1], path, 1, true) then + n = n + 1 + end + end + return n +end +local function offer(w) + local ok = driver_command("ev_set_current", w, + {phase_mode="auto", voltage=230, max_amps_per_phase=16}) + assert(ok, "ev_set_current failed at " .. tostring(w) .. " W") +end +local function advance(ms) host._millis_counter = host._millis_counter + ms end + +advance(1000) +offer(3000) +assert(posts("pause_charging") == 0, "first command of a session paused the charger") + +advance(120000) -- past the 90 s phase hold +offer(9000) +assert(posts("pause_charging") == 1, "mid-session flip did not pause") +assert(posts("resume_charging") == 0, "resumed in the same command as the flip pause") + +advance(5000) +offer(9000) +assert(posts("resume_charging") == 0, "resumed before the pause could reach the charger") + +advance(10000) +offer(9000) +assert(posts("resume_charging") == 1, "did not resume once the flip delay passed") +advance(5000) +offer(9000) +assert(posts("resume_charging") == 1, "resumed again after the flip completed") + +print("Easee phase flip: passed") diff --git a/drivers/tests/test_easee_cloud_phase_flip.py b/drivers/tests/test_easee_cloud_phase_flip.py new file mode 100644 index 0000000..b4c6d8b --- /dev/null +++ b/drivers/tests/test_easee_cloud_phase_flip.py @@ -0,0 +1,12 @@ +"""A mid-session Easee phase flip must give the pause time to land.""" +from pathlib import Path +import subprocess + + +def test_easee_cloud_delays_resume_after_phase_flip_pause(): + root = Path(__file__).resolve().parents[2] + result = subprocess.run( + [str(root / "lua55"), "drivers/tests/lua_harness/test_easee_cloud_phase_flip.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 3dff6c9..848da2d 100644 --- a/index.yaml +++ b/index.yaml @@ -193,15 +193,15 @@ drivers: size_bytes: 4054 sha256: "4a2cd1efb4a5583468ce897ebd88a000e348917295c40210e86ecf834c0ba543" - name: "easee_cloud" - version: "1.3.6" + version: "1.3.7" tier: core protocol: http connectivity: cloud setup: [vendor_portal] ders: [ev] control: true - size_bytes: 41070 - sha256: "4f33149c0640c2e9d2c043e36fdf967d8b297f07da56aec934c21e81a397d22b" + size_bytes: 42172 + sha256: "953094e9a731ce527533569b7a8526223b0965a2bcfebff811fb93aa84ad53d9" - name: "esphome_dsmr" version: "1.0.7" tier: core diff --git a/manifests/easee_cloud.yaml b/manifests/easee_cloud.yaml index b50d5a4..7fde731 100644 --- a/manifests/easee_cloud.yaml +++ b/manifests/easee_cloud.yaml @@ -1,5 +1,5 @@ name: "easee_cloud" -version: "1.3.6" +version: "1.3.7" 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: 41070 +size_bytes: 42172 dkb_id: "easee_cloud" -sha256: "4f33149c0640c2e9d2c043e36fdf967d8b297f07da56aec934c21e81a397d22b" +sha256: "953094e9a731ce527533569b7a8526223b0965a2bcfebff811fb93aa84ad53d9" signature: "" bytecode_sha256: "" diff --git a/support-status.json b/support-status.json index 5179564..1aa513e 100644 --- a/support-status.json +++ b/support-status.json @@ -443,7 +443,7 @@ }, { "catalog_source": true, - "catalog_version": "1.3.6", + "catalog_version": "1.3.7", "driver_id": "easee_cloud", "targets": { "blixt-l1": { From f7e19a1ca63a50c1946d12dc238fb434f6198c80 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 4 Oct 2026 15:45:42 +0000 Subject: [PATCH 2/3] fix(easee_cloud): resume a phase flip only after charging stops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resume_charging in the same command continues the session, so a 1Φ→3Φ flip never latches. Wait for a poll that shows charging has stopped instead of a fixed 15 s delay. Co-authored-by: Fredrik Ahlgren Signed-off-by: Fredrik Ahlgren --- CHANGELOG.md | 16 ++--- drivers/lua/easee_cloud.lua | 59 ++++++++++++------- .../test_easee_cloud_phase_flip.lua | 22 +++++-- drivers/tests/test_easee_cloud_phase_flip.py | 4 +- 4 files changed, 63 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bdb1d..ad43068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,14 @@ ## easee_cloud 1.3.7 -Wait 15 s after the pause before resuming a mid-session 1Φ↔3Φ flip. Easee -applies a new `phaseMode` only when the session restarts, and the cloud queues -commands, so the resume sent ~0.3 s after the pause in the same command -reached the charger before the contactor opened. On an Easee Home with a -Tesla, `phaseMode=3` was written mid-session and the car drew 15 A on one -phase for hours until the owner paused and resumed in the Easee app. The -following `ev_set_current` ticks now resume once the delay has passed; an -explicit `ev_pause` or `ev_resume` clears it. Not yet verified on hardware. +Resume a mid-session 1Φ↔3Φ flip only after a poll shows charging has stopped. +Easee applies a new `phaseMode` only when the session restarts, and +`resume_charging` in the same command continues that session, so the car stays +on the old phase count. On an Easee Home with a Tesla, `phaseMode=3` was +written mid-session and the car drew 15 A on one phase for hours until the +owner paused and resumed in the Easee app. A later `ev_set_current` resumes +once a poll has seen the charger leave charging. An explicit `ev_pause` or +`ev_resume` clears the wait. Not yet verified on hardware. ## heishamon 0.8.0 diff --git a/drivers/lua/easee_cloud.lua b/drivers/lua/easee_cloud.lua index 7b5d8c8..30184f2 100644 --- a/drivers/lua/easee_cloud.lua +++ b/drivers/lua/easee_cloud.lua @@ -73,15 +73,15 @@ local last_amps_set = nil local paused_state = false -- A mid-session phase flip only takes effect when the charger restarts --- the session. The cloud queues commands, so a resume that follows the --- pause within the same driver_command (~0.3 s) reached the charger before --- the contactor opened; the session went on at the old phase count. --- Field case, Easee Home + Tesla: phaseMode=3 written mid-session, the car --- drew 15 A on one phase for hours until a pause/resume in the Easee app. --- So hold the auto-resume until the pause has had time to land. Later --- ev_set_current ticks (every ~5 s from the host) resume once it expires. -local PHASE_FLIP_RESUME_DELAY_MS = 15000 -local phase_flip_resume_at_ms = nil +-- the session. resume_charging in the same driver_command continues that +-- session, so the new phaseMode never latches. Field case, Easee Home + +-- Tesla: phaseMode=3 was written mid-session and the car drew 15 A on one +-- phase for hours until a pause/resume in the Easee app. Hold the +-- auto-resume until a later poll shows charging has stopped. +local phase_flip_awaiting_stop = false +local phase_flip_pause_ms = nil +local last_poll_ms = nil +local last_poll_charging = nil -- command_stalled_since_ms tracks when we last wrote a non-zero amps -- offer that did NOT translate into actual charging. Used to surface @@ -614,6 +614,8 @@ function driver_poll() local session_wh = (obs[OBS_SESSION_ENERGY] or 0) * 1000 -- kWh → Wh local connected = (op_mode >= 2 and op_mode <= 6) local charging = (op_mode == 3) + last_poll_ms = host.millis() + last_poll_charging = charging -- op_mode 0 is the sentinel Easee emits when the cloud hasn't heard -- from the unit recently. Anything else means the charger itself is -- responsive even when no car is plugged in. @@ -798,20 +800,26 @@ function driver_command(action, power_w, cmd) if action == "ev_start" then local ok = post_command("/commands/start_charging") - if ok then paused_state = false end + if ok then + paused_state = false + phase_flip_awaiting_stop = false + phase_flip_pause_ms = nil + end return ok elseif action == "ev_pause" then local ok = post_command("/commands/pause_charging") if ok then paused_state = true - phase_flip_resume_at_ms = nil + phase_flip_awaiting_stop = false + phase_flip_pause_ms = nil end return ok elseif action == "ev_resume" then local ok = post_command("/commands/resume_charging") if ok then paused_state = false - phase_flip_resume_at_ms = nil + phase_flip_awaiting_stop = false + phase_flip_pause_ms = nil end return ok elseif action == "ev_set_current" then @@ -838,16 +846,18 @@ function driver_command(action, power_w, cmd) -- contactor while a session is charging: the phase count is only -- latched when a session (re)starts. So on a real mid-session flip -- pause first; the phaseMode write reconfigures, and the auto- - -- resume below (amps > 0 && paused_state) re-closes the contactor - -- on the new phase count once PHASE_FLIP_RESUME_DELAY_MS has - -- passed. Operator-confirmed: a manual pause+resume was the only - -- thing that flipped 1Φ→3Φ. Skip on the first command of a - -- session (last_sent_phases == nil) — there's no live contactor - -- to recycle. 2026-05-30. + -- resume below re-closes the contactor only after a later poll + -- shows charging has stopped. resume_charging in this same call + -- would continue the session, so 1Φ→3Φ would never latch. + -- Operator-confirmed: a manual pause+resume was the only thing + -- that flipped 1Φ→3Φ. Skip on the first command of a session + -- (last_sent_phases == nil) — there's no live contactor to + -- recycle. 2026-05-30. if last_sent_phases ~= nil then if post_command("/commands/pause_charging") then paused_state = true - phase_flip_resume_at_ms = now_ms + PHASE_FLIP_RESUME_DELAY_MS + phase_flip_awaiting_stop = true + phase_flip_pause_ms = now_ms host.log("info", "Easee: pause to flip phaseMode " .. tostring(last_sent_phases) .. "→" .. tostring(requested_phases)) end @@ -896,11 +906,16 @@ function driver_command(action, power_w, cmd) -- the new offer is > 0. Idempotent on the Easee side; -- a failure here doesn't fail the command (the offer -- itself succeeded, controller will retry next tick). - local flip_settling = phase_flip_resume_at_ms ~= nil and now_ms < phase_flip_resume_at_ms - if amps > 0 and paused_state and not flip_settling then + local flip_stopped = phase_flip_awaiting_stop + and last_poll_ms ~= nil + and phase_flip_pause_ms ~= nil + and last_poll_ms > phase_flip_pause_ms + and last_poll_charging == false + if amps > 0 and paused_state and (not phase_flip_awaiting_stop or flip_stopped) then if post_command("/commands/resume_charging") then paused_state = false - phase_flip_resume_at_ms = nil + phase_flip_awaiting_stop = false + phase_flip_pause_ms = nil host.log("info", "Easee: auto-resumed after pause (offer=" .. tostring(amps) .. " A)") end diff --git a/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua b/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua index 5c40d1a..a816ccc 100644 --- a/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua +++ b/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua @@ -1,7 +1,7 @@ dofile("drivers/tests/lua_harness/host_mock.lua") -- A mid-session phase flip only takes effect when the charger restarts the --- session. Resuming in the same command as the pause reached the charger --- before the contactor opened, so the car stayed on one phase. +-- session. resume_charging in the same command as the pause continues that +-- session, so the car stays on one phase until a poll shows charging stopped. local driver = "drivers/lua/easee_cloud.lua" host.reset() host._millis_step = 0 @@ -28,6 +28,14 @@ local function offer(w) assert(ok, "ev_set_current failed at " .. tostring(w) .. " W") end local function advance(ms) host._millis_counter = host._millis_counter + ms end +local function poll(mode, power_kw) + host._http_responses["/observations?ids="] = host.json_encode({ + {id=109,value=mode,timestamp="2026-09-25T00:21:00Z"}, + {id=120,value=power_kw,timestamp="2026-09-25T00:21:20Z"}, + {id=121,value=1,timestamp="2026-09-25T00:21:20Z"}, + }) + driver_poll() +end advance(1000) offer(3000) @@ -38,13 +46,15 @@ offer(9000) assert(posts("pause_charging") == 1, "mid-session flip did not pause") assert(posts("resume_charging") == 0, "resumed in the same command as the flip pause") -advance(5000) +advance(1000) +poll(3, 3.5) offer(9000) -assert(posts("resume_charging") == 0, "resumed before the pause could reach the charger") +assert(posts("resume_charging") == 0, "resumed while the poll still showed charging") -advance(10000) +advance(1000) +poll(2, 0) offer(9000) -assert(posts("resume_charging") == 1, "did not resume once the flip delay passed") +assert(posts("resume_charging") == 1, "did not resume once a poll showed charging had stopped") advance(5000) offer(9000) assert(posts("resume_charging") == 1, "resumed again after the flip completed") diff --git a/drivers/tests/test_easee_cloud_phase_flip.py b/drivers/tests/test_easee_cloud_phase_flip.py index b4c6d8b..17cd12d 100644 --- a/drivers/tests/test_easee_cloud_phase_flip.py +++ b/drivers/tests/test_easee_cloud_phase_flip.py @@ -1,9 +1,9 @@ -"""A mid-session Easee phase flip must give the pause time to land.""" +"""A mid-session Easee phase flip resumes only after charging has stopped.""" from pathlib import Path import subprocess -def test_easee_cloud_delays_resume_after_phase_flip_pause(): +def test_easee_cloud_resumes_phase_flip_after_charging_stops(): root = Path(__file__).resolve().parents[2] result = subprocess.run( [str(root / "lua55"), "drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua"], From 9d67fabfe4e78621f97dee23c96c729eecee81ba Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 4 Oct 2026 18:07:24 +0200 Subject: [PATCH 3/3] fix(easee_cloud): clear phase-flip stop proof when polling fails Signed-off-by: Fredrik Ahlgren --- CHANGELOG.md | 3 ++- drivers/lua/easee_cloud.lua | 4 ++++ .../lua_harness/test_easee_cloud_phase_flip.lua | 14 ++++++++++++++ index.yaml | 4 ++-- manifests/easee_cloud.yaml | 4 ++-- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad43068..93f1b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ on the old phase count. On an Easee Home with a Tesla, `phaseMode=3` was written mid-session and the car drew 15 A on one phase for hours until the owner paused and resumed in the Easee app. A later `ev_set_current` resumes once a poll has seen the charger leave charging. An explicit `ev_pause` or -`ev_resume` clears the wait. Not yet verified on hardware. +`ev_resume` clears the wait. A failed poll clears the stop observation, so +auto-resume waits for a valid poll again. Not yet verified on hardware. ## heishamon 0.8.0 diff --git a/drivers/lua/easee_cloud.lua b/drivers/lua/easee_cloud.lua index 30184f2..2c96a9a 100644 --- a/drivers/lua/easee_cloud.lua +++ b/drivers/lua/easee_cloud.lua @@ -594,6 +594,10 @@ function driver_init(config) end function driver_poll() + -- A failed poll leaves the charger state unknown. A prior stop + -- observation must not allow a phase-flip resume through that gap. + last_poll_ms = nil + last_poll_charging = nil if not charger_serial or not email then return 10000 end diff --git a/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua b/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua index a816ccc..97258af 100644 --- a/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua +++ b/drivers/tests/lua_harness/test_easee_cloud_phase_flip.lua @@ -51,6 +51,20 @@ poll(3, 3.5) offer(9000) assert(posts("resume_charging") == 0, "resumed while the poll still showed charging") +advance(1000) +poll(2, 0) +advance(1000) +poll(0, 0) +offer(9000) +assert(posts("resume_charging") == 0, "offline poll left an old stop observation valid") + +advance(1000) +poll(2, 0) +host._http_responses["/observations?ids="] = "not JSON" +driver_poll() +offer(9000) +assert(posts("resume_charging") == 0, "failed poll left an old stop observation valid") + advance(1000) poll(2, 0) offer(9000) diff --git a/index.yaml b/index.yaml index 848da2d..8638919 100644 --- a/index.yaml +++ b/index.yaml @@ -200,8 +200,8 @@ drivers: setup: [vendor_portal] ders: [ev] control: true - size_bytes: 42172 - sha256: "953094e9a731ce527533569b7a8526223b0965a2bcfebff811fb93aa84ad53d9" + size_bytes: 42813 + sha256: "408b48baf27f1aa4e622e699dca77cea2b34074178bf4e86ce5ffdd8c045d2dd" - name: "esphome_dsmr" version: "1.0.7" tier: core diff --git a/manifests/easee_cloud.yaml b/manifests/easee_cloud.yaml index 7fde731..bafe691 100644 --- a/manifests/easee_cloud.yaml +++ b/manifests/easee_cloud.yaml @@ -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: 42172 +size_bytes: 42813 dkb_id: "easee_cloud" -sha256: "953094e9a731ce527533569b7a8526223b0965a2bcfebff811fb93aa84ad53d9" +sha256: "408b48baf27f1aa4e622e699dca77cea2b34074178bf4e86ce5ffdd8c045d2dd" signature: "" bytecode_sha256: ""