From af00063f47e3062b88d79103330d01b568ac2880 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 6 Sep 2026 13:13:57 +0200 Subject: [PATCH 1/3] feat(easee): verify active session identity for saved battery levels Signed-off-by: Fredrik Ahlgren --- CHANGELOG.md | 2 + SUPPORT_STATUS.md | 4 +- devices.yaml | 2 +- drivers/lua/easee_cloud.lua | 79 ++++++++++++++++++- .../lua_harness/test_easee_cloud_session.lua | 65 +++++++++++++++ drivers/tests/test_easee_cloud_session.py | 12 +++ index.yaml | 6 +- manifests/easee_cloud.yaml | 16 +++- support-status.json | 2 +- 9 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 drivers/tests/lua_harness/test_easee_cloud_session.lua create mode 100644 drivers/tests/test_easee_cloud_session.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f78fd4..1eaea9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html ### Changed +- **`easee_cloud` 1.3.0** — verify an active energy session before core can retain a confirmed battery level across restart. Keep that identity through pauses in the same driver process. A paused or finished session cannot prove that the same car remains after restart. Failed or empty observations emit no telemetry. + - **easee_cloud** 1.2.0 — emit `request_active`: false only when the vehicle side has explicitly stopped requesting current (Easee `reasonForNoCurrent` 50, or `op_mode` 4 "completed"); every box-ordered pause (52/53/100, pending diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index 189d28e..0d232d9 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.2.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| easee_cloud | 1.2.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| easee_cloud | 1.3.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| easee_cloud | 1.3.0 | 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 b12e82f..8a58a7e 100644 --- a/devices.yaml +++ b/devices.yaml @@ -349,7 +349,7 @@ manufacturers: protocols: - protocol: http driver: "easee_cloud" - version: "1.2.0" + version: "1.3.0" ders: [ev] control: true firmware_versions: "" diff --git a/drivers/lua/easee_cloud.lua b/drivers/lua/easee_cloud.lua index 75a7fe4..09b0087 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.2.0", + version = "1.3.0", protocols = { "http" }, capabilities = { "ev" }, description = "Easee Home/Charge via Cloud REST API. No local protocol needed.", @@ -277,15 +277,16 @@ local OBS_SESSION_ENERGY = 121 local OBS_LIFETIME_ENERGY = 124 local OBS_CURRENT = 183 local OBS_VOLTAGE = 194 +local OBS_SESSION_START = 223 -local OBS_IDS = "48,96,103,109,120,121,124,183,194" +local OBS_IDS = "48,96,103,109,120,121,124,183,194,223" local function get_observations(serial) local url = "https://api.easee.com/state/" .. serial .. "/observations?ids=" .. OBS_IDS local resp, err = safe_http_get(url, auth_headers()) if err then return nil, err end local decoded = safe_json_decode(resp) - if not decoded then return nil, "decode failed" end + if type(decoded) ~= "table" then return nil, "decode failed" end local list = decoded.observations or decoded local obs = {} for _, item in ipairs(list) do @@ -293,9 +294,80 @@ local function get_observations(serial) obs[item.id] = tonumber(item.value) or item.value end end + local mode = obs[OBS_OP_MODE] + if type(mode) ~= "number" or mode < 0 or mode > 6 or mode % 1 ~= 0 then + return nil, "missing charger state" + end + if mode >= 2 and (type(obs[OBS_TOTAL_POWER]) ~= "number" or + type(obs[OBS_SESSION_ENERGY]) ~= "number" or obs[OBS_SESSION_ENERGY] < 0) then + return nil, "missing session measurement" + end return obs, nil end +-- Observation 223 identifies an energy session. That session can end before +-- the cable is removed, so it is not enough to restore a prior car's level. +-- Validate an active session against the sessions API once, then retain its +-- identity through pauses in this driver process. A fresh process waits for +-- active-session proof again. Neither endpoint proves a paused car's identity. +-- https://developer.easee.com/docs/charger-observation-ids +-- https://developer.easee.com/reference/chargers_getongoingsessiondetails +local validated_session_id = nil +local observed_session_id = nil +local session_lookups_ms = {} +local last_session_lookup_ms = nil + +local function normalized_session_start(value) + if type(value) ~= "string" or + not value:match("^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d") then return nil end + return (value:gsub("%.0+Z$", "Z"):gsub("%+00:00$", "Z")) +end + +local function current_session_id(obs, op_mode) + if op_mode == 0 or op_mode == 1 then + validated_session_id = nil + observed_session_id = nil + return nil + end + local raw = obs[OBS_SESSION_START] + local session = type(raw) == "table" and raw or nil + if type(raw) == "string" then session = safe_json_decode(raw) end + if not session then return nil end + local id = tonumber(session.Id) + local start = normalized_session_start(session.Start) + if not id or id <= 0 or id % 1 ~= 0 or not start then return nil end + local identity = string.format("%.0f", id) .. ":" .. start + if identity ~= observed_session_id then + observed_session_id = identity + validated_session_id = nil + end + if validated_session_id == identity then return identity end + -- A completed or awaiting session can belong to an earlier car. Do not + -- turn that into automatic battery-level restoration after restart. + if op_mode ~= 3 or (obs[OBS_TOTAL_POWER] or 0) < 0.1 then return nil end + + -- The vendor allows ten requests per hour, not one per normal poll. + local now = host.millis() + local recent = {} + for _, at in ipairs(session_lookups_ms) do + if now - at < 3600000 then table.insert(recent, at) end + end + session_lookups_ms = recent + if #recent >= 10 or (last_session_lookup_ms and now - last_session_lookup_ms < 60000) then + return nil + end + last_session_lookup_ms = now + table.insert(session_lookups_ms, now) + local body, err = safe_http_get(BASE_URL .. "/chargers/" .. charger_serial .. "/sessions/ongoing", auth_headers()) + if err then return nil end + local ongoing = safe_json_decode(body) + if not ongoing or tonumber(ongoing.sessionId) ~= id or + normalized_session_start(ongoing.sessionStart) ~= start or + (ongoing.sessionEnd ~= nil and ongoing.sessionEnd ~= "") then return nil end + validated_session_id = identity + return identity +end + -- ---- State mapping ---- -- Easee chargerOpMode (observation 109): -- 1 = disconnected (standby) @@ -570,6 +642,7 @@ function driver_poll() charging = charging, request_active = request_active, session_wh = session_wh, + session_id = current_session_id(obs, op_mode), op_mode = op_mode, -- 1=disc,2=awaiting,3=charging,4=completed,5=error,6=ready state_label = OP_MODE_LABELS[op_mode] or "unknown", reason_no_current = reason_code, -- int: 0=ok; why NOT drawing current diff --git a/drivers/tests/lua_harness/test_easee_cloud_session.lua b/drivers/tests/lua_harness/test_easee_cloud_session.lua new file mode 100644 index 0000000..f86e72a --- /dev/null +++ b/drivers/tests/lua_harness/test_easee_cloud_session.lua @@ -0,0 +1,65 @@ +dofile("drivers/tests/lua_harness/host_mock.lua") +local driver = "drivers/lua/easee_cloud.lua" +local serial = "TEST123" +local start = "2026-01-01T08:00:00.000Z" +local function boot(ended) + host.reset() + host._http_responses["/accounts/login"] = '{"accessToken":"test","expiresIn":3600}' + host._http_responses["/config"] = '{}' + host._http_responses["/sessions/ongoing"] = host.json_encode({sessionId=100,sessionStart="2026-01-01T08:00:00Z",sessionEnd=ended and "2026-01-01T09:00:00Z" or nil}) + dofile(driver) + driver_init({email="test@example.invalid",password="test",serial=serial}) +end +local function poll(mode, session, energy, lifetime) + local obs = { + {id=109,value=mode}, {id=120,value=4.3}, + {id=121,value=energy or 9}, {id=124,value=lifetime or 1009}, + } + if session ~= nil then table.insert(obs,{id=223,value=session}) end + host._http_responses["/observations?ids="] = host.json_encode(obs) + driver_poll() + local rows=host._emitted.ev + assert(rows and #rows>0, "no EV sample") + return rows[#rows] +end +local canonical = "100:2026-01-01T08:00:00Z" +local current = host.json_encode({Id=100,Start=start,MeterValue=1000}) +boot() +assert(poll(3,current).session_id == canonical,"current session missing") +assert(poll(4,current).session_id == canonical,"paused/full session lost") +boot() +assert(poll(3,current).session_id == canonical,"identity changed on driver restart") +assert(poll(2,current).session_id == canonical,"a pause lost the confirmed session") +boot() +assert(poll(2,current).session_id == nil,"restart inferred a paused car's identity") +boot(true) +assert(poll(3,current).session_id == nil,"closed API session passed as active") +boot() +assert(poll(3,current,9,1000).session_id == canonical,"lagging lifetime counter blocked a verified active session") +for i=1,20 do assert(poll(3,current).session_id == canonical) end +local lookups=0 +for _, call in ipairs(host._calls) do + if call.func=="http_get" and call.args[1]:find("/sessions/ongoing",1,true) then lookups=lookups+1 end +end +assert(lookups==1,"validated session repeatedly queried the rate-limited API") +local next = host.json_encode({Id=101,Start="2026-01-02T08:00:00.000Z",MeterValue=1009}) +host._millis_counter = host._millis_counter + 60000 +host._http_responses["/sessions/ongoing"] = host.json_encode({sessionId=101,sessionStart="2026-01-02T08:00:00Z"}) +assert(poll(3,next,1,1010).session_id == "101:2026-01-02T08:00:00Z","new session did not get its own verified identity") +assert(poll(1,current).session_id == nil,"disconnected charger has session identity") +assert(poll(3,current,1,1010).session_id == nil,"a prior session bypassed active-session confirmation") +assert(poll(3,nil).session_id == nil,"missing identity invented") +assert(poll(3,'not-json').session_id == nil,"malformed identity accepted") +assert(poll(3,'{"Id":100,"Start":"yesterday","MeterValue":1000}').session_id == nil,"invalid session time accepted") + +local before = #host._emitted.ev +host._http_responses["/observations?ids="] = '{}' +driver_poll() +assert(#host._emitted.ev == before,"empty observations invented an unplug") +host._http_responses["/observations?ids="] = 'not-json' +driver_poll() +assert(#host._emitted.ev == before,"broken JSON invented fresh telemetry") +host._http_responses["/observations?ids="] = nil +driver_poll() +assert(#host._emitted.ev == before,"failed read invented fresh telemetry") +print("Easee current-session identity: passed") diff --git a/drivers/tests/test_easee_cloud_session.py b/drivers/tests/test_easee_cloud_session.py new file mode 100644 index 0000000..1808eb5 --- /dev/null +++ b/drivers/tests/test_easee_cloud_session.py @@ -0,0 +1,12 @@ +"""A saved battery level needs the current hardware session, not its history.""" +from pathlib import Path +import subprocess + + +def test_easee_cloud_current_session_identity(): + root = Path(__file__).resolve().parents[2] + result = subprocess.run( + [str(root / "lua55"), "drivers/tests/lua_harness/test_easee_cloud_session.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 33c32d3..fb8d380 100644 --- a/index.yaml +++ b/index.yaml @@ -193,15 +193,15 @@ drivers: size_bytes: 4054 sha256: "4a2cd1efb4a5583468ce897ebd88a000e348917295c40210e86ecf834c0ba543" - name: "easee_cloud" - version: "1.2.0" + version: "1.3.0" tier: core protocol: http connectivity: cloud setup: [vendor_portal] ders: [ev] control: true - size_bytes: 32848 - sha256: "ea3c8b158ebef1ad2f8a3f94a82e2826438231e8fe04c5957546ee836b370ec8" + size_bytes: 36192 + sha256: "cf4c561d3da2a9832804d4846233dfe246ebb6a9f094feea5b1a12858515b163" - name: "esphome-dsmr" version: "1.0.3" tier: community diff --git a/manifests/easee_cloud.yaml b/manifests/easee_cloud.yaml index df34bf3..74e9eb8 100644 --- a/manifests/easee_cloud.yaml +++ b/manifests/easee_cloud.yaml @@ -1,5 +1,5 @@ name: "easee_cloud" -version: "1.2.0" +version: "1.3.0" tier: core author: "Sourceful Labs AB" protocol: http @@ -16,9 +16,19 @@ 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: 32848 +size_bytes: 36192 dkb_id: "easee_cloud" -sha256: "ea3c8b158ebef1ad2f8a3f94a82e2826438231e8fe04c5957546ee836b370ec8" +sha256: "cf4c561d3da2a9832804d4846233dfe246ebb6a9f094feea5b1a12858515b163" signature: "" bytecode_sha256: "" + +upstream_docs: + - url: "https://developer.easee.com/docs/charger-observation-ids" + title: "Easee charger observations" + kind: api_docs + url_stability: stable + - url: "https://developer.easee.com/reference/chargers_getongoingsessiondetails" + title: "Easee ongoing session API" + kind: api_docs + url_stability: stable diff --git a/support-status.json b/support-status.json index 26baff7..d1b8c85 100644 --- a/support-status.json +++ b/support-status.json @@ -590,7 +590,7 @@ }, { "catalog_source": true, - "catalog_version": "1.2.0", + "catalog_version": "1.3.0", "driver_id": "easee_cloud", "package_id": null, "targets": { From f92e70501608fd3a6567b507a00493dbf6e85b84 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 6 Sep 2026 13:17:02 +0200 Subject: [PATCH 2/3] test(easee): enforce the hourly session lookup limit Signed-off-by: Fredrik Ahlgren --- drivers/tests/lua_harness/test_easee_cloud_session.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/tests/lua_harness/test_easee_cloud_session.lua b/drivers/tests/lua_harness/test_easee_cloud_session.lua index f86e72a..927c17d 100644 --- a/drivers/tests/lua_harness/test_easee_cloud_session.lua +++ b/drivers/tests/lua_harness/test_easee_cloud_session.lua @@ -62,4 +62,14 @@ assert(#host._emitted.ev == before,"broken JSON invented fresh telemetry") host._http_responses["/observations?ids="] = nil driver_poll() assert(#host._emitted.ev == before,"failed read invented fresh telemetry") +boot(true) +for i=1,20 do + host._millis_counter = host._millis_counter + 61000 + assert(poll(3,current).session_id == nil,"ended session accepted during retries") +end +lookups=0 +for _, call in ipairs(host._calls) do + if call.func=="http_get" and call.args[1]:find("/sessions/ongoing",1,true) then lookups=lookups+1 end +end +assert(lookups==10,"ongoing-session API exceeded ten requests per hour: "..lookups) print("Easee current-session identity: passed") From 3890ca922a627fe0eee5df825a4fefe95cefd1b9 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 6 Sep 2026 15:15:17 +0200 Subject: [PATCH 3/3] fix(easee): validate session payloads and expire completed proof Signed-off-by: Fredrik Ahlgren --- CHANGELOG.md | 2 +- drivers/lua/easee_cloud.lua | 14 +++--- .../lua_harness/test_easee_cloud_session.lua | 48 ++++++++++++++++++- index.yaml | 4 +- manifests/easee_cloud.yaml | 4 +- 5 files changed, 59 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eaea9e..249bfd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html ### Changed -- **`easee_cloud` 1.3.0** — verify an active energy session before core can retain a confirmed battery level across restart. Keep that identity through pauses in the same driver process. A paused or finished session cannot prove that the same car remains after restart. Failed or empty observations emit no telemetry. +- **`easee_cloud` 1.3.0** — verify an active energy session before core can retain a confirmed battery level across restart. Keep that identity through pauses in the same driver process, but clear it on completion until a new active session proves its identity. An offline car may need its battery level confirmed after completion or restart; do not reuse another car's level. Match equivalent UTC timestamp formats from both session APIs. Failed or empty observations emit no telemetry. Invalid session payloads omit the identity while keeping fresh charger readings. - **easee_cloud** 1.2.0 — emit `request_active`: false only when the vehicle side has explicitly stopped requesting current (Easee `reasonForNoCurrent` diff --git a/drivers/lua/easee_cloud.lua b/drivers/lua/easee_cloud.lua index 09b0087..5eb44a2 100644 --- a/drivers/lua/easee_cloud.lua +++ b/drivers/lua/easee_cloud.lua @@ -308,8 +308,10 @@ end -- Observation 223 identifies an energy session. That session can end before -- the cable is removed, so it is not enough to restore a prior car's level. -- Validate an active session against the sessions API once, then retain its --- identity through pauses in this driver process. A fresh process waits for --- active-session proof again. Neither endpoint proves a paused car's identity. +-- identity through pauses in this driver process until a completed session. +-- Completion or a fresh process needs active proof again; an offline car may +-- need its battery level confirmed. Neither endpoint proves a paused car's +-- identity after an ended session. -- https://developer.easee.com/docs/charger-observation-ids -- https://developer.easee.com/reference/chargers_getongoingsessiondetails local validated_session_id = nil @@ -320,11 +322,11 @@ local last_session_lookup_ms = nil local function normalized_session_start(value) if type(value) ~= "string" or not value:match("^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d") then return nil end - return (value:gsub("%.0+Z$", "Z"):gsub("%+00:00$", "Z")) + return (value:gsub("%+00:00$", "Z"):gsub("%.0+Z$", "Z")) end local function current_session_id(obs, op_mode) - if op_mode == 0 or op_mode == 1 then + if op_mode == 0 or op_mode == 1 or op_mode == 4 then validated_session_id = nil observed_session_id = nil return nil @@ -332,7 +334,7 @@ local function current_session_id(obs, op_mode) local raw = obs[OBS_SESSION_START] local session = type(raw) == "table" and raw or nil if type(raw) == "string" then session = safe_json_decode(raw) end - if not session then return nil end + if type(session) ~= "table" then return nil end local id = tonumber(session.Id) local start = normalized_session_start(session.Start) if not id or id <= 0 or id % 1 ~= 0 or not start then return nil end @@ -361,7 +363,7 @@ local function current_session_id(obs, op_mode) local body, err = safe_http_get(BASE_URL .. "/chargers/" .. charger_serial .. "/sessions/ongoing", auth_headers()) if err then return nil end local ongoing = safe_json_decode(body) - if not ongoing or tonumber(ongoing.sessionId) ~= id or + if type(ongoing) ~= "table" or tonumber(ongoing.sessionId) ~= id or normalized_session_start(ongoing.sessionStart) ~= start or (ongoing.sessionEnd ~= nil and ongoing.sessionEnd ~= "") then return nil end validated_session_id = identity diff --git a/drivers/tests/lua_harness/test_easee_cloud_session.lua b/drivers/tests/lua_harness/test_easee_cloud_session.lua index 927c17d..0c2d5f8 100644 --- a/drivers/tests/lua_harness/test_easee_cloud_session.lua +++ b/drivers/tests/lua_harness/test_easee_cloud_session.lua @@ -12,7 +12,7 @@ local function boot(ended) end local function poll(mode, session, energy, lifetime) local obs = { - {id=109,value=mode}, {id=120,value=4.3}, + {id=109,value=mode}, {id=120,value=mode == 3 and 4.3 or 0}, {id=121,value=energy or 9}, {id=124,value=lifetime or 1009}, } if session ~= nil then table.insert(obs,{id=223,value=session}) end @@ -26,10 +26,11 @@ local canonical = "100:2026-01-01T08:00:00Z" local current = host.json_encode({Id=100,Start=start,MeterValue=1000}) boot() assert(poll(3,current).session_id == canonical,"current session missing") -assert(poll(4,current).session_id == canonical,"paused/full session lost") +assert(poll(4,current).session_id == nil,"completed session retained active proof") boot() assert(poll(3,current).session_id == canonical,"identity changed on driver restart") assert(poll(2,current).session_id == canonical,"a pause lost the confirmed session") +assert(poll(6,current).session_id == canonical,"ready mode lost the confirmed session") boot() assert(poll(2,current).session_id == nil,"restart inferred a paused car's identity") boot(true) @@ -72,4 +73,47 @@ for _, call in ipairs(host._calls) do if call.func=="http_get" and call.args[1]:find("/sessions/ongoing",1,true) then lookups=lookups+1 end end assert(lookups==10,"ongoing-session API exceeded ten requests per hour: "..lookups) + +-- A car can be swapped between polls while observation 223 still describes +-- its predecessor. Once completion was seen, pauses cannot reuse that proof. +boot() +assert(poll(3,current).session_id == canonical) +host._http_responses["/sessions/ongoing"] = host.json_encode({sessionId=100,sessionStart="2026-01-01T08:00:00Z",sessionEnd="2026-01-01T09:00:00Z"}) +assert(poll(4,current).session_id == nil,"completion kept the previous car's proof") +assert(poll(2,current).session_id == nil,"awaiting car reused completed session") +assert(poll(6,current).session_id == nil,"ready car reused completed session") +host._millis_counter = host._millis_counter + 61000 +assert(poll(3,current).session_id == nil,"ended API session passed active revalidation") +host._millis_counter = host._millis_counter + 61000 +host._http_responses["/sessions/ongoing"] = host.json_encode({sessionId=101,sessionStart="2026-01-02T08:00:00Z"}) +assert(poll(3,next,1,1010).session_id == "101:2026-01-02T08:00:00Z","new active session did not regain proof") + +-- The two APIs may encode the same UTC instant differently. +for _, pair in ipairs({ + {observation="2026-01-01T08:00:00.000+00:00", ongoing="2026-01-01T08:00:00Z"}, + {observation="2026-01-01T08:00:00Z", ongoing="2026-01-01T08:00:00.000+00:00"}, +}) do + boot() + host._http_responses["/sessions/ongoing"] = host.json_encode({sessionId=100,sessionStart=pair.ongoing}) + local observation = host.json_encode({Id=100,Start=pair.observation}) + assert(poll(3,observation).session_id == canonical,"equivalent UTC times did not match") +end +boot() +host._http_responses["/sessions/ongoing"] = host.json_encode({sessionId=100,sessionStart="2026-01-01T08:00:01.000+00:00"}) +assert(poll(3,current).session_id == nil,"different start times passed session proof") + +-- Valid JSON can still have the wrong shape. Keep fresh charger readings +-- when either session endpoint sends a scalar or a document without an ID. +for _, payload in ipairs({"true", "false", "42", '"error"', "null", "[]", "{}"}) do + boot() + local sample = poll(3, payload) + assert(sample.session_id == nil, "wrong-shaped observation accepted: " .. payload) + assert(sample.connected and sample.w == 4300, "session payload dropped fresh charger readings") + + boot() + host._http_responses["/sessions/ongoing"] = payload + sample = poll(3, current) + assert(sample.session_id == nil, "wrong-shaped ongoing session accepted: " .. payload) + assert(sample.connected and sample.w == 4300, "ongoing payload dropped fresh charger readings") +end print("Easee current-session identity: passed") diff --git a/index.yaml b/index.yaml index fb8d380..6ddc4d7 100644 --- a/index.yaml +++ b/index.yaml @@ -200,8 +200,8 @@ drivers: setup: [vendor_portal] ders: [ev] control: true - size_bytes: 36192 - sha256: "cf4c561d3da2a9832804d4846233dfe246ebb6a9f094feea5b1a12858515b163" + size_bytes: 36344 + sha256: "f127426d39ccf831d132f401d0b7d676efa76568974eed3096b6c4569492ee23" - name: "esphome-dsmr" version: "1.0.3" tier: community diff --git a/manifests/easee_cloud.yaml b/manifests/easee_cloud.yaml index 74e9eb8..1ab60f1 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: 36192 +size_bytes: 36344 dkb_id: "easee_cloud" -sha256: "cf4c561d3da2a9832804d4846233dfe246ebb6a9f094feea5b1a12858515b163" +sha256: "f127426d39ccf831d132f401d0b7d676efa76568974eed3096b6c4569492ee23" signature: "" bytecode_sha256: ""