Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html

### Changed

- **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
authorization, schedules, fuse limits) stays true. Lets the FTW host tell
"the car declined" from "we paused it": the session-completion latch stops
the planner allocating energy to a full car, a manual Start hold
auto-releases instead of offering power all night, and the
charging-interrupted notification stops firing on the car's own
renegotiation bursts. Field-observed 2026-08-29: a car at its own charge
limit held reason 50 overnight while the box kept offering 11 kW and paged
the operator twice.

- **zap** 3.1.0 — P1/HAN remains the default. `read_pv` and `read_battery` are opt-in, read-only ingest of devices Zap already talks to (closed inverter Modbus, or an RS-485 bus Zap owns). Off by default so a native FTW driver is not doubled. The driver still never writes. Chargers stay out: add those in FTW.
- **`nibe_local` 1.1.3** — heat-pump diagnostic metrics convert vendor kW/kWh to W/Wh at emit (case and surrounding spaces folded, so `kW ` still converts). Headline names `hp_energy_consumed_kwh` and `hp_energy_produced_kwh` stay so existing series keys do not move; the unit field is Wh. `DRIVER.read_only = true` so the signed artifact matches the observe-only command path. HTTP GET and JSON decode wrap in `pcall`.
- **`myuplink` 1.2.1** — bulk kW/kWh points and the `hp_power_w` headline convert to W/Wh at emit. There are no `hp_energy_*_kwh` headlines; energy, if the pump reports it, is a sanitized bulk name with unit Wh.
Expand Down
4 changes: 2 additions & 2 deletions SUPPORT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no |
| easee_cloud | 1.1.1 | 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 |
| 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 |
Expand Down
2 changes: 1 addition & 1 deletion devices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ manufacturers:
protocols:
- protocol: http
driver: "easee_cloud"
version: "1.1.1"
version: "1.2.0"
ders: [ev]
control: true
firmware_versions: ""
Expand Down
52 changes: 43 additions & 9 deletions drivers/lua/easee_cloud.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ DRIVER = {
id = "easee-cloud",
name = "Easee Cloud",
manufacturer = "Easee",
version = "1.0.1",
version = "1.2.0",
protocols = { "http" },
capabilities = { "ev" },
description = "Easee Home/Charge via Cloud REST API. No local protocol needed.",
Expand Down Expand Up @@ -181,6 +181,22 @@ local function redact_http_err(err)
return tostring(err):match("^(HTTP %d+)") or "request failed"
end

-- Wrapped transport. A host whose http_get raises, or a vendor API
-- that answers with garbage, must degrade to (nil, err) — never kill
-- the driver mid-poll.
local function safe_http_get(url, headers)
local ok, resp, err = pcall(host.http_get, url, headers)
if not ok then return nil, tostring(resp) end
return resp, err
end

local function safe_json_decode(s)
if s == nil then return nil end
local ok, data = pcall(host.json_decode, s)
if not ok then return nil end
return data
end

-- ---- Auth helpers ----

local function login(email, password)
Expand All @@ -190,7 +206,7 @@ local function login(email, password)
host.log("error", "Easee login failed: " .. redact_http_err(err))
return false
end
local data = host.json_decode(resp)
local data = safe_json_decode(resp)
if not data or not data.accessToken then
host.log("error", "Easee login: no accessToken in response")
return false
Expand All @@ -215,7 +231,7 @@ local function do_refresh()
host.log("warn", "Easee token refresh failed: " .. redact_http_err(err))
return false
end
local data = host.json_decode(resp)
local data = safe_json_decode(resp)
if not data or not data.accessToken then
host.log("warn", "Easee refresh: no accessToken, will re-login")
return false
Expand Down Expand Up @@ -246,9 +262,9 @@ end
-- ---- API helpers ----

local function get_chargers()
local resp, err = host.http_get(BASE_URL .. "/chargers", auth_headers())
local resp, err = safe_http_get(BASE_URL .. "/chargers", auth_headers())
if err then return nil, err end
return host.json_decode(resp), nil
return safe_json_decode(resp), nil
end

-- Observation IDs (from developer.easee.com/docs/charger-observation-ids)
Expand All @@ -266,9 +282,9 @@ local OBS_IDS = "48,96,103,109,120,121,124,183,194"

local function get_observations(serial)
local url = "https://api.easee.com/state/" .. serial .. "/observations?ids=" .. OBS_IDS
local resp, err = host.http_get(url, auth_headers())
local resp, err = safe_http_get(url, auth_headers())
if err then return nil, err end
local decoded = host.json_decode(resp)
local decoded = safe_json_decode(resp)
if not decoded then return nil, "decode failed" end
local list = decoded.observations or decoded
local obs = {}
Expand Down Expand Up @@ -355,9 +371,9 @@ local email, password, configured_max_a
-- (compare requested phaseMode after a write) is a follow-up; this
-- driver's contribution is the diagnostic logging at init.
local function read_settings(serial)
local resp, err = host.http_get(BASE_URL .. "/chargers/" .. serial .. "/config", auth_headers())
local resp, err = safe_http_get(BASE_URL .. "/chargers/" .. serial .. "/config", auth_headers())
if err then return nil, redact_http_err(err) end
local decoded = host.json_decode(resp)
local decoded = safe_json_decode(resp)
if decoded == nil then
return nil, "decode failed (non-JSON response)"
end
Expand Down Expand Up @@ -531,10 +547,28 @@ function driver_poll()
command_stalled_since_ms = 0
end

-- request_active: false ONLY when the vehicle side has explicitly
-- stopped requesting current — Easee reason 50 ("secondary unit
-- not requesting current"; the vehicle is the secondary unit) or
-- op_mode 4 ("completed"). Every other reason (52/53/100 = the box
-- throttled it, pending authorization, schedules, fuse limits)
-- keeps the default true, so a pause the box itself ordered is
-- never mistaken for the car declining. The host debounces this
-- for 90 s before acting on it (session-completion latch,
-- manual-hold auto-release), so a brief 50 during session
-- handshake is harmless. Field observation 2026-08-29: a car at
-- its own charge limit held reason 50 all night while the box kept
-- offering 11 kW and paged the operator twice about it.
local request_active = true
if connected and not charging and (reason_code == 50 or op_mode == 4) then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register the Easee enumeration as upstream documentation

This new interpretation of Easee reason 50 and operation mode 4 depends on vendor-defined enumeration semantics, but manifests/easee_cloud.yaml still has no upstream_docs entry. Because .github/workflows/watch-upstream-docs.yml only watches URLs declared in manifests, changes or removal of the documentation underlying request_active will go unnoticed; add the durable Easee enumeration/API reference to the manifest.

AGENTS.md reference: AGENTS.md:L60-L66

Useful? React with 👍 / 👎.

request_active = false
end

host.emit("ev", {
w = power_w,
connected = connected,
charging = charging,
request_active = request_active,
session_wh = session_wh,
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",
Expand Down
6 changes: 3 additions & 3 deletions index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,15 @@ drivers:
size_bytes: 4054
sha256: "4a2cd1efb4a5583468ce897ebd88a000e348917295c40210e86ecf834c0ba543"
- name: "easee_cloud"
version: "1.1.1"
version: "1.2.0"
tier: core
protocol: http
connectivity: cloud
setup: [vendor_portal]
ders: [ev]
control: true
size_bytes: 31312
sha256: "dfe3107d6d99e03204184602fd2be973efb53b7f122d4ad1dd3cbe1d0be5ebd4"
size_bytes: 32848
sha256: "ea3c8b158ebef1ad2f8a3f94a82e2826438231e8fe04c5957546ee836b370ec8"
- name: "esphome-dsmr"
version: "1.0.3"
tier: community
Expand Down
6 changes: 3 additions & 3 deletions manifests/easee_cloud.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "easee_cloud"
version: "1.1.1"
version: "1.2.0"
tier: core
author: "Sourceful Labs AB"
protocol: http
Expand All @@ -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: 31312
size_bytes: 32848
dkb_id: "easee_cloud"
sha256: "dfe3107d6d99e03204184602fd2be973efb53b7f122d4ad1dd3cbe1d0be5ebd4"
sha256: "ea3c8b158ebef1ad2f8a3f94a82e2826438231e8fe04c5957546ee836b370ec8"
signature: ""

bytecode_sha256: ""
2 changes: 1 addition & 1 deletion support-status.json
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@
},
{
"catalog_source": true,
"catalog_version": "1.1.1",
"catalog_version": "1.2.0",
"driver_id": "easee_cloud",
"package_id": null,
"targets": {
Expand Down