Skip to content
Draft
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
96 changes: 96 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,107 @@
# Changelog

## [Unreleased]

### easee_cloud 1.3.8

- Reject power observations that move backwards in source time, and conflicting values at the same physical time. Compare RFC3339 instants with UTC offsets and fractional precision normalized. An old cloud zero can no longer replace a newer sample, nor can an old positive sample resurrect an observed stop.
- Connected chargers with missing or malformed power timestamps emit no fresh measurement. Unchanged valid values remain confirmable, and newer zero observations remain valid. A confirmed disconnect resets the session ordering.
- Reproduced with synthetic cloud responses; not yet verified on hardware. This does not prove that delayed packets caused every field balance mismatch. No command policy or charging limit is changed.

## easee_cloud 1.3.7

- Keep a load-balancing ReasonForNoCurrent while the car charges, when the per-phase current is at least 2 A below the offer. Easee records the reason when it changes, so a limit that began before the last power change was dropped and FTW said the charger had given no reason. The offer is observation 48, or FTW's last write when that is missing. Other old reasons are still dropped while the car charges.
- Report `current_limited_by = "load_balancer"` for codes 1–5, 10 and 25–30 while a car is connected. Core reads this name, not Easee's codes. The charger's own limits (77, 78) and the car are not the load balancer.
- Match the labels for codes 6, 8–11, 25–30 and 77 to Easee's published table. Code 28 read "fuse limit reached"; Easee calls it "Current limited by Equalizer". The enumeration page joins `upstream_docs`.
- Not yet seen on hardware. Easee's table defines the "Current limited by …" codes, but no raw observation from a load-balanced charger has been captured.
### home_assistant_vehicle 0.1.0

A new read-only driver for a car whose level Home Assistant already reads. A
Home Assistant automation publishes one JSON message over MQTT; the driver
subscribes, emits every 5 seconds and holds no Home Assistant token. It
reads the level, charge limit and charging state, and, when the automation
maps them, the charge cable (`plugged`) and the car's place (`at_site` from
a tracker in zone home). The automation may add `plugged_since` from
`last_changed`, but only when the integration reads the cable at least
every 30 seconds: `last_changed` is when Home Assistant read the change.
The header carries the automation for the Tesla Custom Integration.
Messages from a Model Y on that integration, recorded on 15 October 2026,
are the test fixtures.

### tesla_cloud 0.2.0

Report the charge cable (`plugged`, from `conn_charge_cable`, else the
charging state). No cable time: the driver reads the car minutes apart. With
a site position from FTW and the optional `vehicle_location` scope, the same
`vehicle_data` read asks for `location_data`, and the driver emits the car's
distance from the site (`site_distance_m`), never its coordinates. A token
without the scope gets HTTP 403 for that read; the driver then reads
`charge_state` alone and asks again a day later. No request is added and no
car is woken. A level of 1 % goes as 0.01.

### teslamate_vehicle 0.2.0

Report the charge cable from TeslaMate's `plugged_in`. A change is dated
(`plugged_since_ms`, by the box's clock) only when a live update carries it:
a `healthy` message within 10 seconds. A retained value, at subscribe or
redelivered after the broker reconnects, has no known time. With `geofence`
set to this site's TeslaMate geofence, the geofence topic gives `at_site`;
otherwise `latitude` and `longitude` give the distance from the site. The
coordinates are not emitted. A level of 1 % goes as 0.01.

### vag_vehicle 0.3.0

Report the charge cable from the Data Act dataset's plug state. No cable
time: the portal writes every 15 minutes. A plug state other than connected
or disconnected is unknown, not unplugged. The datasets carry no position.
A level of 1 % goes as 0.01.

### tesla_vehicle 0.2.6

A level of 1 % goes as 0.01. FTW reads a vehicle `soc` of 1 as 100 %, so a
car at 1 % looked full.

### pixii_pv 0.3.3

Report zero PV generation when the measured phase sum is negative. Negating
that sum used to emit positive PV power, which FTW rejects, so fresh readings
could leave PV stale or missing after sunset. Sum the phases before limiting
generation to zero; keep their signed diagnostic values. Missing, invalid or
non-finite phase power emits no PV reading. Idle polls do not refresh PV.

## pixii 2.1.9

A power cut left the driver offline until someone restarted it. Each register
keeps its own failure count and stops being read after three misses. When the
Pixii boots slower than FTW, every read fails for about fifteen seconds,
three polls at five seconds, and every register is retired. The device then
answers and the driver never asks again.

Misses count on a poll where some other register answered. A poll where every
read fails leaves the counts unchanged: the device is unreachable. The next
poll that gets an answer clears the counts. Register 40288 on PowerShaper
firmware below CPU 2.0.23 still stops after three misses while the device is
answering other registers, and is tried again every five minutes.

FTW's bundled copy does not move with this commit. Bump the pin and run
`scripts/sync-bundled-drivers.sh` (srcfl/ftw#1620).

## sungrow 1.6.0

Read the live values once a second. `driver_poll` returns 1000 ms. Each poll
reads grid, PV and battery power, SoC, per-phase values and the EMS readback.
Energy counters, rated power, temperature, frequency and fault bits keep the
five-second rate. Between those reads each blob carries their last values, but
only when its own power read succeeded this poll. FTW stores each emit whole,
and its energy ledger starts over whenever a counter drops out.

A host that ignores the returned interval still polls every five seconds, and
then every poll reads everything, as before. FTW honours the return from
srcfl/ftw#1616.

A read FTW refused while it waited to reconnect no longer counts toward
treating a register as absent. At one poll per second, a two-second wait would
otherwise mark a whole block absent after one dropped packet.

## Tests: no invented numbers

Expand Down
34 changes: 18 additions & 16 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.7 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| easee_cloud | 1.3.7 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| easee_cloud | 1.3.8 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| easee_cloud | 1.3.8 | 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 Expand Up @@ -80,6 +80,8 @@ Catalog source is not proof that a target can install or run a driver.
| heishamon | 0.8.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| hello | 1.1.2 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| hello | 1.1.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| home_assistant_vehicle | 0.1.0 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| home_assistant_vehicle | 0.1.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| huawei | 2.1.3 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| huawei | 2.1.3 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| janitza | 1.0.5 | ftw-core | not_assessed | — | not_recorded | not_assessed |
Expand Down Expand Up @@ -112,10 +114,10 @@ Catalog source is not proof that a target can install or run a driver.
| p1_hdlc | 2.0.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| p1_meter | 2.0.2 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| p1_meter | 2.0.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| pixii | 2.1.8 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| pixii | 2.1.8 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| pixii_pv | 0.3.2 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| pixii_pv | 0.3.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| pixii | 2.1.9 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| pixii | 2.1.9 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| pixii_pv | 0.3.3 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| pixii_pv | 0.3.3 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| remne_p1ib | 0.1.0 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| remne_p1ib | 0.1.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| saj | 1.2.0 | ftw-core | not_assessed | — | not_recorded | not_assessed |
Expand Down Expand Up @@ -156,20 +158,20 @@ Catalog source is not proof that a target can install or run a driver.
| solis_string | 1.1.3 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| sonnen | 2.0.3 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| sonnen | 2.0.3 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| sungrow | 1.5.10 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| sungrow | 1.5.10 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| tesla_cloud | 0.1.1 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| tesla_cloud | 0.1.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| tesla_vehicle | 0.2.5 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| tesla_vehicle | 0.2.5 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| sungrow | 1.6.0 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| sungrow | 1.6.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| tesla_cloud | 0.2.0 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| tesla_cloud | 0.2.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| tesla_vehicle | 0.2.6 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| tesla_vehicle | 0.2.6 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| tesla_wall_connector | 0.1.1 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| tesla_wall_connector | 0.1.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| teslamate_vehicle | 0.1.1 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| teslamate_vehicle | 0.1.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| teslamate_vehicle | 0.2.0 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| teslamate_vehicle | 0.2.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| tibber | 1.1.2 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| tibber | 1.1.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| vag_vehicle | 0.2.1 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| vag_vehicle | 0.2.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| vag_vehicle | 0.3.0 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| vag_vehicle | 0.3.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| varta | 1.1.1 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| varta | 1.1.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| victron | 2.1.2 | ftw-core | not_assessed | — | not_recorded | not_assessed |
Expand Down
41 changes: 27 additions & 14 deletions 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.7"
version: "1.3.8"
ders: [ev]
control: true
firmware_versions: ""
Expand Down Expand Up @@ -606,6 +606,19 @@ manufacturers:
control: false
firmware_versions: ""
notes: "Community driver, untested"
- name: "Home Assistant"
model_families:
- name: "Vehicle (Home Assistant MQTT)"
variants: [Model Y]
regions: []
protocols:
- protocol: mqtt
driver: "home_assistant_vehicle"
version: "0.1.0"
ders: [vehicle]
control: false
firmware_versions: ""
notes: "Read-only vehicle level, charge limit, charging state, charge cable and place from a JSON message a Home Assistant automation publishes. Tested with recorded messages from a Model Y on the Tesla Custom Integration; the cable and place fields have not run on a car. Does not wake or command the car."
- name: "Hoymiles"
model_families:
- name: "HM Series"
Expand Down Expand Up @@ -1021,7 +1034,7 @@ manufacturers:
protocols:
- protocol: mqtt
driver: "pixii_pv"
version: "0.3.2"
version: "0.3.3"
ders: [pv, meter]
control: false
firmware_versions: ""
Expand All @@ -1032,7 +1045,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "pixii"
version: "2.1.8"
version: "2.1.9"
ders: [battery, meter]
control: false
firmware_versions: ""
Expand Down Expand Up @@ -1749,7 +1762,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.10"
version: "1.6.0"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand All @@ -1760,7 +1773,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.10"
version: "1.6.0"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand All @@ -1771,7 +1784,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.10"
version: "1.6.0"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand All @@ -1782,7 +1795,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.10"
version: "1.6.0"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand All @@ -1793,7 +1806,7 @@ manufacturers:
protocols:
- protocol: modbus
driver: "sungrow"
version: "1.5.10"
version: "1.6.0"
ders: [battery, meter, pv]
control: true
firmware_versions: ""
Expand All @@ -1806,7 +1819,7 @@ manufacturers:
protocols:
- protocol: http
driver: "tesla_vehicle"
version: "0.2.5"
version: "0.2.6"
ders: [vehicle]
control: true
firmware_versions: ""
Expand All @@ -1817,22 +1830,22 @@ manufacturers:
protocols:
- protocol: http
driver: "tesla_cloud"
version: "0.1.1"
version: "0.2.0"
ders: [vehicle]
control: false
firmware_versions: ""
notes: "Read-only vehicle SoC, charge limit and charging state via the official Tesla Fleet API. Optional next to tesla_vehicle (BLE proxy). Does not wake or command the car. Not yet run against a car."
notes: "Read-only vehicle SoC, charge limit, charging state and charge cable via the official Tesla Fleet API. Grant the optional vehicle_location scope to report the car's distance from the site; the coordinates stay in the driver. Optional next to tesla_vehicle (BLE proxy). Does not wake or command the car. Not yet run against a car."
- name: "Tesla Vehicle (TeslaMate)"
variants: [Model 3, Model Y]
regions: []
protocols:
- protocol: mqtt
driver: "teslamate_vehicle"
version: "0.1.1"
version: "0.2.0"
ders: [vehicle]
control: false
firmware_versions: ""
notes: "Read-only vehicle SoC, charge limit and charging state via TeslaMate MQTT. Optional next to tesla_vehicle (BLE proxy). Does not wake or command the car. Not yet run against a car."
notes: "Read-only vehicle SoC, charge limit, charging state, charge cable and place via TeslaMate MQTT. Set geofence to this site's TeslaMate geofence for the place. Optional next to tesla_vehicle (BLE proxy). Does not wake or command the car. Not yet run against a car."
- name: "Tesla Wall Connector"
variants: [Wall Connector Gen 3]
regions: []
Expand Down Expand Up @@ -2064,7 +2077,7 @@ manufacturers:
protocols:
- protocol: http
driver: "vag_vehicle"
version: "0.2.1"
version: "0.3.0"
ders: [vehicle]
control: false
firmware_versions: ""
Expand Down
26 changes: 26 additions & 0 deletions docs/WRITING-A-DRIVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,32 @@ make check
`make check` regenerates `index.yaml`, `devices.yaml`, `support-status.json`
and the manifest hashes. Commit what it produces — it fails if those are stale.

## A car says where it is plugged in

A vehicle driver can help FTW tell which car is on a charger. Emit
`plugged`, and when the source knows them, `plugged_since_ms` and `at_site`
or `site_distance_m`. The fields are in
[`spec/host-api.md`](../spec/host-api.md#vehicle-ftw).

- **Date only what you saw happen.** Send `plugged_since_ms` only when the
source reads the cable at least every 30 seconds. A car read every few
minutes shows a plug-in minutes late, and FTW would match it to the wrong
charger or to none. A value the driver saw at its first reading, or one a
broker redelivers after a reconnect, has no known time: leave it out.
- **Prefer the source's own idea of home.** A Home Assistant zone or a
TeslaMate geofence gives `at_site`. Measuring against
`host.site_location()` is the fallback: that position is a weather pin.
- **Keep the coordinates in the driver.** Emit only `site_distance_m`,
rounded to the metre.
- **Unknown is not false.** A cable state or a place the source does not
know is `nil`. A wrong `false` tells FTW the car is somewhere else.
- **No extra reads.** The cable and the place come with the reads the level
already needs. Never wake a car, or add a billed request, to learn them.
- **A level of 1 % goes as 0.01.** FTW reads a vehicle `soc` of 1 as 100 %.

`home_assistant_vehicle`, `tesla_cloud`, `teslamate_vehicle` and
`vag_vehicle` show the patterns.

## Testing

The harness is at `drivers/tests/lua_harness/`. `host_mock.lua` lets you set
Expand Down
Loading
Loading