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

## [Unreleased]

### zap 3.1.2

The P1/HAN site meter reads again on Zap firmware serving the data model v2
local API. That firmware renames every meter field with an `_ac` suffix and
lowercases the `L1`/`L2`/`L3` phase prefix (`W` → `W_ac`, `L1_V` → `l1_V_ac`;
`Hz` is unchanged). `emit_meter` read the v1 names unconditionally, so a Zap
on v2 firmware gave FTW no meter reading at all — `driver_poll` logged
`Zap: site-meter payload unavailable` every cycle and FTW lost its site
meter, with no site meter data for the home's grid import/export.

- `emit_meter` now tries the v1 field name first, then the `_ac` suffix,
then the lowercased-phase-prefix `_ac` variant, so it reads either
firmware generation without checking `version` explicitly.
- Scoped to the meter DER only. `pv`/`battery` (`emit_pv`/`emit_battery`)
have the identical `.W`-style field assumption against v2 and are not
covered here — see #170 for that broader scope (`pv` → `solar` rename,
`_dc`/`_ac` split, `null`-vs-`0` handling).

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

### tesla_wall_connector 0.1.2

An idle Wall Connector no longer shows charge power. With no car on the
Expand Down
4 changes: 2 additions & 2 deletions SUPPORT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ Catalog source is not proof that a target can install or run a driver.
| victron_mqtt | 1.0.3 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| wallbox | 1.0.3 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| wallbox | 1.0.3 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| zap | 3.1.1 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| zap | 3.1.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| zap | 3.1.2 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| zap | 3.1.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| zaptec | 1.0.1 | ftw-core | not_assessed | — | not_recorded | not_assessed |
| zaptec | 1.0.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed |
| zaptec_cloud | 0.1.1 | 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 @@ -1749,7 +1749,7 @@ manufacturers:
protocols:
- protocol: http
driver: "zap"
version: "3.1.1"
version: "3.1.2"
ders: [meter, pv, battery]
control: false
firmware_versions: ""
Expand Down
29 changes: 26 additions & 3 deletions drivers/lua/zap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,22 @@
-- meter: +W import, -W export
-- pv: -W generation
-- battery:+W charge, -W discharge
--
-- Meter schema compatibility: some Zap firmware serves the local data model
-- v2 (srcful-data-models 2.3.0, "version":"v2" in /data/json), which renames
-- every meter field with an "_ac" suffix and lowercases the L1/L2/L3 phase
-- prefix (e.g. "L1_V" -> "l1_V_ac", "W" -> "W_ac"; "Hz" is unchanged either
-- way). This driver reads both v1 and v2 field names for the meter DER; see
-- https://github.com/srcfl/device-drivers/issues/170 for the full v2 scope
-- (pv -> solar, battery, DC/AC split), which is not yet covered here.

DRIVER = {
host_api_min = 1,
host_api_max = 1,
id = "zap",
name = "Sourceful Zap (P1/HAN meter)",
manufacturer = "Sourceful",
version = "3.1.1",
version = "3.1.2",
protocols = { "http" },
capabilities = { "meter", "pv", "battery" },
read_only = true,
Expand Down Expand Up @@ -377,15 +385,30 @@ local meter_fields = {
{ "total_export_wh", "total_export_Wh", "Wh" },
}

-- Data model v2 renames every meter field with an "_ac" suffix and
-- lowercases the L1/L2/L3 phase prefix (e.g. "L1_V" -> "l1_V_ac"); "Hz" is
-- unchanged. Try the v1 name first, then the v2 variants, so this driver
-- reads either firmware generation without needing to branch on "version".
local function raw_field(raw, name)
if raw[name] ~= nil then return raw[name] end
if raw[name .. "_ac"] ~= nil then return raw[name .. "_ac"] end
local lowered = name:gsub("^L(%d)_", "l%1_")
if lowered ~= name then
if raw[lowered] ~= nil then return raw[lowered] end
if raw[lowered .. "_ac"] ~= nil then return raw[lowered .. "_ac"] end
end
return nil
end

local function emit_meter(data)
if type(data) ~= "table" or type(data.meter) ~= "table" then return false end
local raw = data.meter
local w = number(raw.W)
local w = number(raw_field(raw, "W"))
if w == nil then return false end

local meter_reading = { w = w }
for _, mapping in ipairs(meter_fields) do
local value = number(raw[mapping[2]])
local value = number(raw_field(raw, mapping[2]))
if value ~= nil then
meter_reading[mapping[1]] = value
host.emit_metric("meter_" .. mapping[1], value, mapping[3])
Expand Down
93 changes: 93 additions & 0 deletions drivers/tests/lua_harness/test_zap_meter_schema.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
-- Exercise zap.lua's meter field reading against both the v1 and v2 Zap
-- local API field names. Run with the scenario as arg[1]: "v1" or "v2".
--
-- This file is a draft for drivers/tests/lua_harness/test_zap_meter_schema.lua
-- in srcfl/device-drivers — see pr-drafts/README.md for how this fits that
-- repo's `make test-driver ID=zap` harness. Not run against the real harness
-- yet (no local checkout with lua55/pytest); matches the structure of the
-- existing drivers/tests/lua_harness/test_esphome_dsmr_aliases.lua, which
-- solves the same kind of field-name-aliasing problem.

local scenario = arg[1]
if scenario ~= "v1" and scenario ~= "v2" then
error("schema scenario must be v1 or v2")
end

dofile("drivers/tests/lua_harness/host_mock.lua")
host.reset()

local function respond(path, body)
host._http_responses["http://127.0.0.1" .. path] = body
end

-- The device list is unaffected by the v2 meter-field rename (see
-- https://github.com/srcfl/device-drivers/issues/170 — only `ders[].type`
-- for pv/battery changes, not for the P1 meter), so both scenarios use the
-- same /api/devices response.
respond("/api/devices", [[
{"devices":[{"sn":"TESTZAP-P1-1","type":"p1_uart","device_type":"energy_meter",
"ders":[{"type":"meter","enabled":true}]}]}
]])

if scenario == "v1" then
respond("/api/devices/TESTZAP-P1-1/data/json", [[
{"meter":{"W":1850,"Hz":50.0,
"L1_W":600,"L1_V":230.1,"L1_A":2.61,
"L2_W":600,"L2_V":230.4,"L2_A":2.60,
"L3_W":650,"L3_V":229.8,"L3_A":2.83,
"total_import_Wh":58000000,"total_export_Wh":9000000}}
]])
else
respond("/api/devices/TESTZAP-P1-1/data/json", [[
{"meter":{"W_ac":1850,"Hz":50.0,
"l1_W_ac":600,"l1_V_ac":230.1,"l1_A_ac":2.61,
"l2_W_ac":600,"l2_V_ac":230.4,"l2_A_ac":2.60,
"l3_W_ac":650,"l3_V_ac":229.8,"l3_A_ac":2.83,
"total_import_Wh_ac":58000000,"total_export_Wh_ac":9000000},
"version":"v2","format":"p1_uart"}
]])
end

dofile("drivers/lua/zap.lua")

if DRIVER.id ~= "zap" or DRIVER.version ~= "3.1.2" then
error("Zap identity metadata is wrong or version was not bumped")
end
if DRIVER.host_api_min ~= 1 or DRIVER.host_api_max ~= 1 or DRIVER.read_only ~= true then
error("Zap host API or read-only metadata is wrong")
end

driver_init({ host = "127.0.0.1" })
local ok, err = pcall(driver_poll)
if not ok then error("driver_poll failed: " .. tostring(err)) end

local samples = host._emitted.meter
if not samples or #samples ~= 1 then
error("expected exactly one meter sample for scenario " .. scenario
.. ", got " .. tostring(samples and #samples or 0))
end

local meter = samples[1]
local function near(actual, expected)
return type(actual) == "number" and math.abs(actual - expected) < 0.001
end

if not near(meter.w, 1850) then error(scenario .. ": meter.w wrong: " .. tostring(meter.w)) end
if not near(meter.l1_w, 600) or not near(meter.l2_w, 600) or not near(meter.l3_w, 650) then
error(scenario .. ": phase power wrong")
end
if not near(meter.l1_v, 230.1) or not near(meter.l2_v, 230.4) or not near(meter.l3_v, 229.8) then
error(scenario .. ": phase voltage wrong")
end
if not near(meter.l1_a, 2.61) or not near(meter.l2_a, 2.60) or not near(meter.l3_a, 2.83) then
error(scenario .. ": phase current wrong")
end
if not near(meter.freq_hz, 50.0) then error(scenario .. ": Hz wrong (unsuffixed in both schemas)") end
if not near(meter.total_import_wh, 58000000) or not near(meter.import_wh, 58000000) then
error(scenario .. ": total_import_Wh / import_wh alias wrong")
end
if not near(meter.total_export_wh, 9000000) or not near(meter.export_wh, 9000000) then
error(scenario .. ": total_export_Wh / export_wh alias wrong")
end

driver_cleanup()
46 changes: 46 additions & 0 deletions drivers/tests/test_zap_meter_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Exercise zap.lua's meter reading against the v1 and v2 Zap local API schemas.

Draft for drivers/tests/test_zap_meter_schema.py in srcfl/device-drivers,
paired with drivers/tests/lua_harness/test_zap_meter_schema.lua. Modeled on
the existing test_esphome_dsmr_aliases.py, which runs the equivalent
field-aliasing check for a different driver through the same lua55 harness.
"""

from pathlib import Path
import re
import subprocess

import pytest


ROOT = Path(__file__).resolve().parents[2]


def test_zap_source_has_ftw_read_only_contract():
source = (ROOT / "drivers/lua/zap.lua").read_text(encoding="utf-8")
for entrypoint in ("driver_init", "driver_poll", "driver_command",
"driver_default_mode"):
assert re.search(rf"\bfunction\s+{entrypoint}\s*\(", source), entrypoint
assert re.search(r"\bhost_api_min\s*=", source)
assert re.search(r"\bhost_api_max\s*=", source)
assert re.search(r"\bread_only\s*=\s*true\b", source)
assert not re.search(
r"\bhost\.(?:http_post|modbus_write(?:_multi)?|mqtt_publish|serial_write)\s*\(",
source,
), "a read-only driver calls a write-capable host function"


@pytest.mark.parametrize("scenario", ["v1", "v2"])
def test_zap_meter_schema(scenario):
result = subprocess.run(
[
str(ROOT / "lua55"),
"drivers/tests/lua_harness/test_zap_meter_schema.lua",
scenario,
],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
assert result.returncode == 0, result.stdout + result.stderr
6 changes: 3 additions & 3 deletions index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -814,14 +814,14 @@ drivers:
size_bytes: 2842
sha256: "1c0362e0a25f1fe297de838a85089eb7676b25e78a7d6d1d03ccea1258b94403"
- name: "zap"
version: "3.1.1"
version: "3.1.2"
tier: core
protocol: http
connectivity: local
ders: [meter, pv, battery]
control: false
size_bytes: 26911
sha256: "dbbef0e614162219c3774292c647f944d0210c752688165afb9a491244700c19"
size_bytes: 28162
sha256: "5be6d7793a9daa074557182e4333443b382a7a34427dd07143d90f27a8cf01b6"
- name: "zaptec"
version: "1.0.1"
tier: community
Expand Down
6 changes: 3 additions & 3 deletions manifests/zap.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "zap"
version: "3.1.1"
version: "3.1.2"
tier: core
author: "Sourceful Labs AB"
protocol: http
Expand All @@ -15,9 +15,9 @@ tested_devices:
notes: "P1/HAN site meter from a Sourceful Zap. PV and battery telemetry from devices on Zap are opt-in (read_pv / read_battery) and read-only. Prefer a native FTW driver; use Zap as the reader only when it already owns the bus."
min_driver_version: "3.1.0"
min_host_version: "2.0.0"
size_bytes: 26911
size_bytes: 28162
dkb_id: "zap"
sha256: "dbbef0e614162219c3774292c647f944d0210c752688165afb9a491244700c19"
sha256: "5be6d7793a9daa074557182e4333443b382a7a34427dd07143d90f27a8cf01b6"
signature: ""

bytecode_sha256: ""
2 changes: 1 addition & 1 deletion support-status.json
Original file line number Diff line number Diff line change
Expand Up @@ -1850,7 +1850,7 @@
},
{
"catalog_source": true,
"catalog_version": "3.1.1",
"catalog_version": "3.1.2",
"driver_id": "zap",
"targets": {
"blixt-l1": {
Expand Down
Loading