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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

## [Unreleased]

### easee_cloud 1.3.7

Start a 6 A order at 7 A. On an Easee Home with an Audi e-tron (three
phases), a resume at 6 A gave a positive pilot only 5 min 2 s to 5 min 47 s
later in four runs on 22 October 2026, also with a single resume; 7 A and
then 6 A started at once. evcc reports the same delay at 6 A. The driver now
offers 7 A per phase when a stopped charger gets a 6 A order, and only when
7 A fits: Core's `cap_amps_per_phase` for this charger, the order's
`max_amps_per_phase` and the charger's static limit, as read from its
config, must all allow 7 A. `max_amps_per_phase` is the fuse rating, not the
room left, so a 6 A order on a phase with room for 6.4 A would otherwise draw
7 A and Core would stop the car. A Core that does not send
`cap_amps_per_phase` (srcfl/ftw#1809) gets no boost. Every order during a
boost checks this again, and one that no longer fits writes 6 A at once. The
driver writes 6 A as soon as the charger reports charging, or after 90 s,
also when observations fail. One boost per start; a running session lowered
to 6 A is not raised, orders above 6 A are unchanged, and no extra resume is
sent. `start_boost: false` turns it off. Tested in the Lua harness; not yet
on hardware with this driver.

### home_assistant_vehicle 0.1.0

A new read-only driver for a car whose level Home Assistant already reads. A
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 |
| 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 |
Expand Down
2 changes: 1 addition & 1 deletion devices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Expand Down
115 changes: 111 additions & 4 deletions drivers/lua/easee_cloud.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
-- email: "user@example.com"
-- password: "secret"
-- serial: "EHHZBKPF" # optional — auto-detected if omitted
-- start_boost: false # optional — disable the 7 A start for 6 A orders

DRIVER = {
host_api_min = 1,
host_api_max = 1,
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.",
Expand Down Expand Up @@ -87,6 +88,23 @@ local command_stalled_since_ms = 0
local EASEE_MIN_A = 6
local EASEE_MAX_A = 32

-- 6 A start boost. Field data (22 Oct 2026, Easee Home + Audi e-tron, 3Φ,
-- four runs): after resume_charging with dynamicChargerCurrent=6 the
-- charger reported a positive pilot only 5 min 2 s – 5 min 47 s later, even
-- with a single resume. Starting at 7 A and lowering to 6 A once the car
-- drew current started at once. evcc reports the same ~5 min firmware
-- delay and clamps the dynamic current to 7 A before its start command.
-- Here the boost is one amp per phase, held only until the charger reports
-- charging (op_mode 3) or START_BOOST_MAX_MS passes, then 6 A. It is only
-- used when 7 A fits (start_boost_fits), and at most once per start.
-- `start_boost: false` disables it.
local START_BOOST_A = EASEE_MIN_A + 1
local START_BOOST_MAX_MS = 90000
local start_boost_enabled = true
local boost_until_ms = nil -- non-nil while 7 A is offered for a 6 A order
local boost_spent = false -- one boost per start; reset by a 0 A order or a stop
local last_op_mode = nil -- op_mode from the latest successful poll

-- pick_phases is the driver-level phase decision: given the requested
-- charging power, the per-phase fuse ceiling, the operator's mode
-- preference, and the time since the last flip, return how many
Expand Down Expand Up @@ -489,6 +507,35 @@ local function write_setting(serial, body_table)
return err
end

-- start_boost_fits: 7 A per phase fits when Core's cap for this charger
-- (cap_amps_per_phase: its room below the fuse and the import limit, after
-- other chargers' share), the order's max_amps_per_phase and the charger's
-- static limit, read from /config, all allow it. max_amps_per_phase alone is
-- the fuse rating, not the room left: a 6 A order on a phase with room for
-- 6.4 A would draw 7 A, Core would see an overage and stop the car, and the
-- next start would boost again. A Core that sends no cap gets no boost.
-- Checked on every order while a boost runs.
local function start_boost_fits(cmd, max_a)
local cap = cmd and type(cmd.cap_amps_per_phase) == "number" and cmd.cap_amps_per_phase or nil
return cap ~= nil and math.floor(cap) >= START_BOOST_A and
max_a ~= nil and math.floor(max_a) >= START_BOOST_A and
device_limit_a ~= nil and device_limit_a >= START_BOOST_A
end

-- end_start_boost writes the ordered 6 A. A failed write leaves the boost
-- running, so the next poll or order tries again.
local function end_start_boost(why)
local werr = write_setting(charger_serial, {dynamicChargerCurrent = EASEE_MIN_A})
if werr ~= nil then
host.log("warn", "Easee: start boost step-down failed (will retry): " .. redact_http_err(werr))
return false
end
host.log("info", "Easee: start boost ended (" .. why .. "), dynamicChargerCurrent=6 A")
boost_until_ms = nil
last_amps_set = EASEE_MIN_A
return true
end

function driver_init(config)
host.set_make("Easee")

Expand Down Expand Up @@ -525,6 +572,10 @@ function driver_init(config)
end
end

if config and (config.start_boost == false or config.start_boost == "false") then
start_boost_enabled = false
end

if not email or not password then
host.log("error", "Easee: email and password required in driver config")
return
Expand Down Expand Up @@ -592,6 +643,11 @@ function driver_poll()
return 10000
end

-- The boost window ends on time even when observations fail.
if boost_until_ms ~= nil and host.millis() >= boost_until_ms then
end_start_boost("timeout")
end

local obs, err, timestamps = get_observations(charger_serial)
if err or not obs then
host.log("warn", "Easee: observations poll failed: " .. redact_http_err(err))
Expand Down Expand Up @@ -748,6 +804,17 @@ function driver_poll()
end
end

-- 6 A start boost: step down to the ordered 6 A as soon as the charger
-- reports charging. The timeout is checked above, before observations.
if boost_until_ms ~= nil and charging then
end_start_boost("charging")
end
-- A session that stops charging may need a new boost on its next start.
if last_op_mode == 3 and not charging and boost_until_ms == nil then
boost_spent = false
end
last_op_mode = op_mode

if obs[OBS_VOLTAGE] then
host.emit_metric("ev_voltage_v", obs[OBS_VOLTAGE])
end
Expand Down Expand Up @@ -791,7 +858,11 @@ 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
boost_until_ms = nil
boost_spent = false
end
return ok
elseif action == "ev_resume" then
local ok = post_command("/commands/resume_charging")
Expand Down Expand Up @@ -853,9 +924,43 @@ function driver_command(action, power_w, cmd)
end

local amps = per_phase_amps(power_w, voltage, phases, max_a)
local err = write_setting(charger_serial, {dynamicChargerCurrent = amps})
local write_a = amps
local boosting = false
local boost_end = nil
if amps == EASEE_MIN_A then
if boost_until_ms ~= nil then
-- Keep 7 A within the window while it still fits and the
-- charger has not started; do not restart the window.
-- Otherwise this write of 6 A ends the boost.
if now_ms >= boost_until_ms then
boost_end = "timeout"
elseif last_op_mode == 3 then
boost_end = "charging"
elseif not start_boost_fits(cmd, max_a) then
boost_end = "no room for 7 A"
else
write_a, boosting = START_BOOST_A, true
end
elseif start_boost_enabled and not boost_spent and last_op_mode ~= 3 and start_boost_fits(cmd, max_a) then
write_a, boosting = START_BOOST_A, true
end
end
local err = write_setting(charger_serial, {dynamicChargerCurrent = write_a})
if err == nil then
last_amps_set = amps
if boosting then
if boost_until_ms == nil then
boost_until_ms = now_ms + START_BOOST_MAX_MS
boost_spent = true
host.log("info", "Easee: start boost 7 A for a 6 A start (Easee delays ~5 min at 6 A)")
end
else
if boost_end ~= nil then
host.log("info", "Easee: start boost ended (" .. boost_end .. "), dynamicChargerCurrent=6 A")
end
boost_until_ms = nil
if amps == 0 then boost_spent = false end
end
last_amps_set = write_a
-- After a phaseMode change, schedule one re-write of
-- dynamicChargerCurrent on the next driver_poll. Easee's
-- cloud resets it to maxChargerCurrent during the phase
Expand Down Expand Up @@ -904,6 +1009,8 @@ function driver_default_mode()
if post_command("/commands/pause_charging") then
paused_state = true
end
boost_until_ms = nil
boost_spent = false
local err = write_setting(charger_serial, {dynamicChargerCurrent = 0})
if err == nil then
last_amps_set = 0
Expand Down
Loading
Loading