From c9b4a8812a412b0cd50ab06456dbcc602eaaf197 Mon Sep 17 00:00:00 2001 From: Segran Date: Thu, 12 Nov 2026 13:32:20 +0100 Subject: [PATCH] fix(easee_cloud): order power observations across REST and SignalR Signed-off-by: Segran --- CHANGELOG.md | 8 + SUPPORT_STATUS.md | 4 +- devices.yaml | 2 +- drivers/lua/easee_cloud.lua | 178 ++++++++++++++---- .../test_easee_cloud_observation_order.lua | 41 ++++ .../lua_harness/test_easee_cloud_session.lua | 5 +- .../test_easee_cloud_start_boost.lua | 5 +- .../lua_harness/test_easee_cloud_stream.lua | 53 +++++- .../test_easee_cloud_observation_order.py | 12 ++ index.yaml | 6 +- manifests/easee_cloud.yaml | 6 +- support-status.json | 2 +- 12 files changed, 262 insertions(+), 60 deletions(-) create mode 100644 drivers/tests/lua_harness/test_easee_cloud_observation_order.lua create mode 100644 drivers/tests/test_easee_cloud_observation_order.py diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf58aa..320cfec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### easee_cloud 1.4.2 + +REST and SignalR compare TotalPower source times on the same UTC timeline. +Older readings and conflicting values at one instant cannot replace newer +power. A disconnect clears the power watermark. If the source clock moves +back, advancing readings on the lower timeline can restore telemetry after +30 seconds. + ### easee_cloud 1.4.1 The driver says its session energy is coarse, with `session_wh_coarse`. Easee diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index 1574553..b7fae7a 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.4.1 | ftw-core | not_assessed | — | not_recorded | not_assessed | -| easee_cloud | 1.4.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | +| easee_cloud | 1.4.2 | ftw-core | not_assessed | — | not_recorded | not_assessed | +| easee_cloud | 1.4.2 | 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 317220b..7ae9d3d 100644 --- a/devices.yaml +++ b/devices.yaml @@ -338,7 +338,7 @@ manufacturers: protocols: - protocol: http driver: "easee_cloud" - version: "1.4.1" + version: "1.4.2" ders: [ev] control: true firmware_versions: "" diff --git a/drivers/lua/easee_cloud.lua b/drivers/lua/easee_cloud.lua index fd123f9..5fa78e9 100644 --- a/drivers/lua/easee_cloud.lua +++ b/drivers/lua/easee_cloud.lua @@ -29,7 +29,7 @@ DRIVER = { id = "easee_cloud", name = "Easee Cloud", manufacturer = "Easee", - version = "1.4.1", + version = "1.4.2", protocols = { "http" }, capabilities = { "ev" }, description = "Easee Home/Charge via Cloud REST API. No local protocol needed.", @@ -311,34 +311,102 @@ for id in OBS_IDS:gmatch("%d+") do OBS_WANTED[tonumber(id)] = true end -- stays current until a newer one arrives. local latest, latest_at, latest_pushed = {}, {}, {} --- source_time_key makes an RFC 3339 UTC time comparable as a string, --- whatever its fractional precision: "…:05Z" and "…:05.120+00:00" become --- "…:05.120000000" and "…:05.120000000". Other offsets give nil. +-- Compare RFC3339 source instants independently of fractional precision and +-- UTC offset. Wall-clock receipt time must not make an old packet current. local function source_time_key(stamp) if type(stamp) ~= "string" then return nil end - local base, frac, zone = stamp:match("^(%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d)(%.?%d*)(.*)$") - if not base or (zone ~= "Z" and zone ~= "+00:00") then return nil end - return base .. "." .. (frac:sub(2) .. "000000000"):sub(1, 9) + local year, month, day, hour, minute, second, suffix = stamp:match( + "^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)(.*)$") + if not year then return nil end + year, month, day = tonumber(year), tonumber(month), tonumber(day) + hour, minute, second = tonumber(hour), tonumber(minute), tonumber(second) + if year < 1970 or month < 1 or month > 12 or hour > 23 or minute > 59 or second > 59 then return nil end + local leap = year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0) + local lengths = {31, leap and 29 or 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + if day < 1 or day > lengths[month] then return nil end + local fraction = "" + if suffix:sub(1, 1) == "." then + local digits, zone = suffix:match("^%.(%d+)(.*)$") + if not digits then return nil end + fraction, suffix = digits:gsub("0+$", ""), zone + end + local offset = 0 + if suffix ~= "Z" then + local sign, oh, om = suffix:match("^([+-])(%d%d):(%d%d)$") + if not sign then return nil end + oh, om = tonumber(oh), tonumber(om) + if oh > 23 or om > 59 then return nil end + offset = (oh * 60 + om) * 60 * (sign == "+" and 1 or -1) + end + local function leaps(y) + return math.floor(y / 4) - math.floor(y / 100) + math.floor(y / 400) + end + local days = 365 * (year - 1970) + leaps(year - 1) - leaps(1969) + day - 1 + for m = 1, month - 1 do days = days + lengths[m] end + local seconds = hour * 3600 + minute * 60 + second - offset + days = days + math.floor(seconds / 86400) + if days < 0 then return nil end + return string.format("%07d%05d", days, seconds % 86400) .. "." .. fraction +end +local POWER_CLOCK_RECOVERY_MS = 30000 +local power_key, power_value, power_stamp +local recovery_key, recovery_since +local stream_power_problem +local last_power_observed_at + +local function reset_power_order() + power_key, power_value, power_stamp = nil, nil, nil + recovery_key, recovery_since = nil, nil + stream_power_problem, last_power_observed_at = nil, nil + latest[OBS_TOTAL_POWER], latest_at[OBS_TOTAL_POWER], latest_pushed[OBS_TOTAL_POWER] = nil, nil, nil +end + +-- A repeated cached reading cannot move the clock back. Require a +-- strictly advancing source instant after 30 s on the lower timeline. +local function power_problem(value, key, now) + if type(value) ~= "number" or not key then + return "missing or invalid power observation" + end + if power_key and key < power_key then + if not recovery_key or key < recovery_key then + recovery_key, recovery_since = key, now + elseif key > recovery_key and now - recovery_since >= POWER_CLOCK_RECOVERY_MS then + return nil + else + recovery_key = key + end + return "out-of-order power observation" + end + if key == power_key and value ~= power_value then + return "conflicting power at one source timestamp" + end + return nil end --- note_observation records a sample. SignalR delivers one connection's --- messages in order, so a pushed sample always applies. A REST reply can be --- older than what the stream pushed since, cached before the change; then it --- does not apply. Otherwise REST applies as it always has, so one bad stamp --- cannot hold a value. The same instant keeps its first spelling, so a --- source time never looks new because the other source wrote it --- differently. Returns true when REST brought a newer, different value. -local function note_observation(id, value, at, pushed) - if not OBS_WANTED[id] then return false end - local new_key, old_key = source_time_key(at), source_time_key(latest_at[id]) - local newer = false - if new_key and old_key then - if not pushed and latest_pushed[id] and new_key < old_key then return false end - if new_key == old_key then at = latest_at[id] end - newer = new_key > old_key - end - local changed = value ~= latest[id] +-- Apply power ordering across both transports. REST fallback replaces the +-- snapshot but keeps the ordering watermark until an explicit disconnect. +local function note_observation(id, value, at, pushed, now) + local key, held = source_time_key(at), source_time_key(latest_at[id]) + local newer, changed = false, value ~= latest[id] + if id == OBS_TOTAL_POWER then + if latest[OBS_OP_MODE] == 1 then return false end + local problem = power_problem(value, key, now) + if problem then return false, problem end + newer, changed = power_key ~= nil and key > power_key, value ~= power_value + if key == power_key then at = power_stamp end + if power_key and key < power_key then + host.log("info", "Easee: power source clock recovered") + end + power_key, power_value, power_stamp = key, value, at + recovery_key, recovery_since, stream_power_problem = nil, nil, nil + if not pushed and key == held and latest_pushed[id] then return false end + elseif key and held then + if not pushed and latest_pushed[id] and key < held then return false end + if key == held then at = latest_at[id] end + newer = key > held + end latest[id], latest_at[id], latest_pushed[id] = value, at, pushed + if id == OBS_OP_MODE and value == 1 then reset_power_order() end return not pushed and newer and changed end @@ -353,7 +421,6 @@ local function observation_value(value) return tonumber(value) or value end -local last_power_observed_at = nil local validated_session_id = nil local observed_session_id = nil local departed_session_id = nil @@ -622,11 +689,24 @@ local function stream_update(id, value, at, now) local key = source_time_key(at) if not key or (owed_kind(id) and type(value) ~= "number") then if owed_kind(id) then owe(id, "", nil, now, "sent a reading it cannot use") end + if id == OBS_TOTAL_POWER and stream_state == "up" and latest[OBS_OP_MODE] ~= 1 then + stream_power_problem = "missing or invalid power observation" + end return end local held = source_time_key(latest_at[id]) if stream_state ~= "up" and held and key < held then return end - note_observation(id, value, at, true) + if id == OBS_TOTAL_POWER and stream_owed and stream_owed.id == id and + power_key and key < power_key and value == stream_owed.value and value == power_value then + -- A coarser timestamp can settle the stream debt without replacing power. + stream_owed = nil + return + end + local _, problem = note_observation(id, value, at, true, now) + if problem then + stream_power_problem = problem + return + end if stream_owed and stream_owed.id == id and (key >= stream_owed.key or value == stream_owed.value) then stream_owed = nil end @@ -964,24 +1044,40 @@ function driver_poll() local rest_read = false if rest_due then local problem = err or state_problem(values) - rest_next_ms = now + (problem and 10000 or REST_CONFIRM_MS) - if problem then - host.log("warn", "Easee: observations poll failed: " .. (err and redact_http_err(err) or problem)) - -- The cloud answered that it does not hear the charger. - if not err then heard_ms = nil end - else - -- Without the stream the reply is the whole state, as before. + if not problem then + -- Apply mode first so a disconnect clears the previous session's power. if not streaming then latest, latest_at, latest_pushed = {}, {}, {} end - for id in pairs(OBS_WANTED) do - if values[id] ~= nil or stamps[id] ~= nil then - if note_observation(id, values[id], stamps[id], false) and streaming and owed_kind(id) then - owe(id, source_time_key(stamps[id]), values[id], now, "missed a change REST saw") + local function apply(id) + local changed, rejected = note_observation(id, values[id], stamps[id], false, now) + if changed and streaming and owed_kind(id) then + owe(id, source_time_key(stamps[id]), values[id], now, "missed a change REST saw") + end + return rejected + end + apply(OBS_OP_MODE) + if values[OBS_OP_MODE] ~= 1 then + problem = apply(OBS_TOTAL_POWER) + -- Older REST data can be normal while a newer stream is healthy. + if problem == "out-of-order power observation" and streaming and + latest_pushed[OBS_TOTAL_POWER] and not stream_power_problem then + problem = nil + end + end + if not problem then + for id in pairs(OBS_WANTED) do + if id ~= OBS_OP_MODE and id ~= OBS_TOTAL_POWER and + (values[id] ~= nil or stamps[id] ~= nil) then + apply(id) end end + if streaming and not stream_owed then stream_failures = 0 end + heard_ms, rest_read = now, true end - -- A REST read that finds the stream complete resets its back-off. - if streaming and not stream_owed then stream_failures = 0 end - heard_ms, rest_read = now, true + end + rest_next_ms = now + (problem and 10000 or REST_CONFIRM_MS) + if problem then + host.log("warn", "Easee: observations poll failed: " .. (err and redact_http_err(err) or problem)) + if not err then heard_ms = nil end end end @@ -990,7 +1086,7 @@ function driver_poll() -- nothing, as before, so stale values never look current. local vouched = rest_read or (stream_state == "up" and heard_ms ~= nil and now - heard_ms <= REST_FRESH_MS) - local blocked = vouched and state_problem(latest) + local blocked = vouched and (state_problem(latest) or stream_power_problem) if blocked and blocked ~= last_blocked then host.log("warn", "Easee: not reporting streamed state: " .. blocked) end diff --git a/drivers/tests/lua_harness/test_easee_cloud_observation_order.lua b/drivers/tests/lua_harness/test_easee_cloud_observation_order.lua new file mode 100644 index 0000000..13c3739 --- /dev/null +++ b/drivers/tests/lua_harness/test_easee_cloud_observation_order.lua @@ -0,0 +1,41 @@ +dofile("drivers/tests/lua_harness/host_mock.lua") +host.reset() +host._millis_step = 0 +host._http_responses["/accounts/login"] = '{"accessToken":"test","expiresIn":3600}' +host._http_responses["/config"] = '{"maxChargerCurrent":16}' +host._http_responses["/sessions/ongoing"] = '{}' +dofile("drivers/lua/easee_cloud.lua") +driver_init({email="test@example.invalid",password="test",serial="TEST123"}) +local function poll(w, stamp, mode) + host._http_responses["/observations?ids="] = host.json_encode({ + {id=109,value=mode or 3,timestamp="2026-10-17T03:49:10Z"}, + {id=120,value=w,timestamp=stamp}, + {id=121,value=12.5,timestamp="2026-10-17T03:49:10Z"}, + }) + driver_poll() + local rows=host._emitted.ev or {} + return #rows, rows[#rows] +end +local count, row = poll(10.242,"2026-10-17T03:49:08.1Z") +assert(count==1 and row.w==10242,"initial measured power missing") +local n = poll(0,"2026-10-17T03:49:07Z") +assert(n==count,"older cloud zero replaced a newer power observation") +n = poll(0,"2026-10-17T05:49:08.10000000000+02:00") +assert(n==count,"contradictory value at the same physical time was accepted") +n = poll(0,nil) +assert(n==count,"missing source time became fresh zero power") +n = poll(0,"invalid") +assert(n==count,"malformed source time became fresh power") +n = poll(0,"2026-02-30T03:49:08Z") +assert(n==count,"impossible date was accepted") +n,row = poll(10.242,"2026-10-17T05:49:08.10000000000+02:00") +assert(n==count+1 and row.control_power_confirmed,"unchanged valid sample no longer confirmed") +count=n +n,row = poll(0,"2026-10-17T03:49:09.1Z",4) +assert(n==count+1 and row.w==0,"a newer observed stop was ignored") +count=n +n = poll(10.242,"2026-10-17T03:49:08.1Z") +assert(n==count,"old positive power resurrected a stopped charge") +n,row = poll(6.2,"2026-10-17T03:49:10Z") +assert(n==count+1 and row.w==6200,"fresh subsequent power did not recover") +print("Easee observation ordering: passed") diff --git a/drivers/tests/lua_harness/test_easee_cloud_session.lua b/drivers/tests/lua_harness/test_easee_cloud_session.lua index b5ea64e..2726080 100644 --- a/drivers/tests/lua_harness/test_easee_cloud_session.lua +++ b/drivers/tests/lua_harness/test_easee_cloud_session.lua @@ -10,9 +10,12 @@ local function boot(ended) dofile(driver) driver_init({email="test@example.invalid",password="test",serial=serial}) end +local observation_sequence = 0 local function poll(mode, session, energy, lifetime) + observation_sequence = observation_sequence + 1 + local stamp = string.format("2026-01-01T08:00:00.%06dZ", observation_sequence) local obs = { - {id=109,value=mode}, {id=120,value=mode == 3 and 4.3 or 0}, + {id=109,value=mode}, {id=120,value=mode == 3 and 4.3 or 0,timestamp=stamp}, {id=121,value=energy or 9}, {id=124,value=lifetime or 1009}, } if session ~= nil then table.insert(obs,{id=223,value=session}) end diff --git a/drivers/tests/lua_harness/test_easee_cloud_start_boost.lua b/drivers/tests/lua_harness/test_easee_cloud_start_boost.lua index 38ab7c5..eba3845 100644 --- a/drivers/tests/lua_harness/test_easee_cloud_start_boost.lua +++ b/drivers/tests/lua_harness/test_easee_cloud_start_boost.lua @@ -35,9 +35,12 @@ local function count_cmd(path) return n end +local observation_sequence = 0 local function poll(mode, kw) + observation_sequence = observation_sequence + 1 + local stamp = string.format("2026-01-01T08:00:00.%06dZ", observation_sequence) host._http_responses["/observations?ids="] = host.json_encode({ - {id=109,value=mode}, {id=120,value=kw or 0}, {id=121,value=1}, {id=124,value=100}, + {id=109,value=mode}, {id=120,value=kw or 0,timestamp=stamp}, {id=121,value=1}, {id=124,value=100}, }) driver_poll() end diff --git a/drivers/tests/lua_harness/test_easee_cloud_stream.lua b/drivers/tests/lua_harness/test_easee_cloud_stream.lua index f59152b..89322e8 100644 --- a/drivers/tests/lua_harness/test_easee_cloud_stream.lua +++ b/drivers/tests/lua_harness/test_easee_cloud_stream.lua @@ -275,8 +275,8 @@ assert(rest_reads() == reads + 1, "REST stopped after a refused subscription") assert(not host._ws_is_open, "a refused stream stayed open") -- 10. Without the stream a REST reply is the whole state, as in 1.3.8: an --- id it leaves out does not keep an earlier reply's value, and a stamp from --- the future does not hold a value. +-- id it leaves out does not keep an earlier reply's value. A future stamp +-- can be superseded once the lower timeline advances. setup(false) poll() host._http_responses["/observations?ids="] = host.json_encode({ @@ -291,7 +291,13 @@ rest(3, 7.2, "2026-10-24T13:00:00Z") poll(5000) rest(3, 0, "2026-10-24T10:05:00Z") poll(5000) -assert(samples().w == 0, "a REST stamp from the future held its value") +local _, held_count = samples() +poll(30000) +assert(select(2, samples()) == held_count, "a repeated cached sample reset the clock") +rest(3, 0, "2026-10-24T10:05:01Z") +poll(5000) +assert(samples().w == 0 and select(2, samples()) == held_count + 1, + "a REST stamp from the future held its value") -- 11. A change REST sees first is owed by the stream. A push still on its -- way keeps the stream up; one that never comes stops it after 10 s. @@ -316,10 +322,11 @@ assert(logged("missed a change REST saw"), "the missed change was not logged") -- op_mode 0 stops reports and says why. setup(true) connect() +local _, before_invalid = samples() push(update(120, "", "2026-10-24T10:00:50Z"), update(121, "13.0", "2026-10-24 10:00:50")) tick() ev = samples() -assert(ev.w == 7200 and ev.session_wh == 12500, "an unreadable push changed the sample") +assert(select(2, samples()) == before_invalid, "an unreadable push emitted a sample") waited = 0 while tick() == 2000 do waited = waited + 1 @@ -345,6 +352,7 @@ table.insert(host._ws_inbox, whole:sub(1, 20)) table.insert(host._ws_inbox, whole:sub(21) .. host.json_encode({type=3, invocationId=sent(2).invocationId}) .. RS) assert(poll(5000) == 2000, "the stream did not come up") assert(samples().w == 4400, "a message split across frames was lost") +rest(3, 4.4, "2026-10-24T10:00:02Z") push({type=7, error="Server is shutting down", allowReconnect=true}) assert(poll(2000) == 5000, "a close message did not stop the stream") assert(logged("server closed: Server is shutting down"), "the close was not logged") @@ -364,6 +372,7 @@ assert(not host._ws_is_open, "a stream that never subscribed stayed open") setup(true) connect() +rest(3, 7.2, "2026-10-24T10:00:01Z") host._ws_is_open = false assert(tick() == 5000, "a connection the host reports closed stayed up") @@ -410,11 +419,41 @@ until_rest() push(update(120, "5.5", "2026-10-24T10:01:40Z")) for _ = 1, 8 do assert(tick() == 2000, "a push of the owed value with a coarser time stopped the stream") end --- Once up, the stream's order wins: a pushed sample applies even when its --- time is older than the one before it. +-- Live pushes obey source ordering too. A newer measured zero still applies. +local _, before_replay = samples() push(update(120, "4.0", "2026-10-24T10:01:30Z")) tick() -assert(samples().w == 4000, "a pushed sample did not apply") +assert(select(2, samples()) == before_replay, "an older push emitted power") +push(update(120, "4.0", "2026-10-24T11:01:40.5+01:00")) +tick() +assert(select(2, samples()) == before_replay, "an equal-instant conflict emitted power") +push(update(120, "0", "2026-10-24T10:01:41Z")) +tick() +assert(samples().w == 0 and select(2, samples()) == before_replay + 1, "new measured zero was lost") + +-- A mode-1 push clears the watermark and any cached power, even when the +-- charger disconnects and reconnects between polls. +push(update(109, "1", "2026-10-24T10:01:42Z"), + update(109, "3", "2026-10-24T10:01:43Z"), + update(120, "4.0", "2026-10-24T10:01:30Z")) +tick() +assert(samples().w == 4000, "mode 1 did not reset stream power ordering") + +-- A live stream with a lower clock recovers once its source time advances. +setup(true) +connect() +push(update(120, "1.0", "2040-10-24T10:00:00Z")) +tick() +local _, before_jump = samples() +push(update(120, "2.0", "2026-10-24T10:01:01Z")) +tick() +assert(select(2, samples()) == before_jump, "a backwards clock jump was accepted immediately") +for _ = 1, 15 do tick() end +assert(select(2, samples()) == before_jump, "rejected stream power was re-emitted") +push(update(120, "2.5", "2026-10-24T10:01:33Z")) +tick() +assert(samples().w == 2500 and select(2, samples()) == before_jump + 1, + "advancing stream power did not recover after a clock jump") -- The replayed current state does not undo a newer REST read. setup(true) diff --git a/drivers/tests/test_easee_cloud_observation_order.py b/drivers/tests/test_easee_cloud_observation_order.py new file mode 100644 index 0000000..dbc168c --- /dev/null +++ b/drivers/tests/test_easee_cloud_observation_order.py @@ -0,0 +1,12 @@ +"""Late cloud packets must not replace a newer power observation.""" +from pathlib import Path +import subprocess + + +def test_easee_cloud_preserves_power_source_order(): + root = Path(__file__).resolve().parents[2] + result = subprocess.run( + [str(root / "lua55"), "drivers/tests/lua_harness/test_easee_cloud_observation_order.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 0f09d7f..df76711 100644 --- a/index.yaml +++ b/index.yaml @@ -193,15 +193,15 @@ drivers: size_bytes: 4054 sha256: "4a2cd1efb4a5583468ce897ebd88a000e348917295c40210e86ecf834c0ba543" - name: "easee_cloud" - version: "1.4.1" + version: "1.4.2" tier: core protocol: http connectivity: cloud setup: [vendor_portal] ders: [ev] control: true - size_bytes: 64392 - sha256: "de954b416dcab6d9d7af06075056dce30a1e9d8dbba80223d78a1cdb806101cd" + size_bytes: 68558 + sha256: "a27883fd51872fbb4fe1469ef25931ec72146a38fd85670d385d31e9426ccb9f" - name: "esphome_dsmr" version: "1.0.7" tier: core diff --git a/manifests/easee_cloud.yaml b/manifests/easee_cloud.yaml index 8ab261a..5965f9a 100644 --- a/manifests/easee_cloud.yaml +++ b/manifests/easee_cloud.yaml @@ -1,5 +1,5 @@ name: "easee_cloud" -version: "1.4.1" +version: "1.4.2" 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: 64392 +size_bytes: 68558 dkb_id: "easee_cloud" -sha256: "de954b416dcab6d9d7af06075056dce30a1e9d8dbba80223d78a1cdb806101cd" +sha256: "a27883fd51872fbb4fe1469ef25931ec72146a38fd85670d385d31e9426ccb9f" signature: "" bytecode_sha256: "" diff --git a/support-status.json b/support-status.json index 65543b7..0974740 100644 --- a/support-status.json +++ b/support-status.json @@ -443,7 +443,7 @@ }, { "catalog_source": true, - "catalog_version": "1.4.1", + "catalog_version": "1.4.2", "driver_id": "easee_cloud", "targets": { "blixt-l1": {