diff --git a/.changeset/zap-save-read-pv-off.md b/.changeset/zap-save-read-pv-off.md
new file mode 100644
index 000000000..11ad1df16
--- /dev/null
+++ b/.changeset/zap-save-read-pv-off.md
@@ -0,0 +1,5 @@
+---
+"ftw": patch
+---
+
+Save Zap PV and battery reads as off when the boxes are unchecked. Before, setup and Settings left `read_pv` out, so Core waited for PV readings the Zap never sends. Open Settings → Devices and save once to fix a Zap added before this release.
diff --git a/web/settings/tabs/devices.js b/web/settings/tabs/devices.js
index fdd5a97be..d08ca6ba8 100644
--- a/web/settings/tabs/devices.js
+++ b/web/settings/tabs/devices.js
@@ -1206,6 +1206,14 @@
'';
}
if (isLocalHTTP) {
+ var isZap = (d.lua || '').indexOf('zap.lua') >= 0;
+ if (isZap) {
+ // Zap reads PV and battery only on opt-in. An unchecked box must
+ // save false: Core treats a missing read_pv as PV that may report.
+ d.config = d.config || {};
+ if (d.config.read_pv == null) d.config.read_pv = false;
+ if (d.config.read_battery == null) d.config.read_battery = false;
+ }
var lcfg = d.config || {};
// NIBE-style local-API drivers (catalog apicreds + a connection port)
// also need a username + an optional self-signed cert pin; plain
@@ -1219,7 +1227,7 @@
html += '
HTTP ' +
'Host / IP ' + help('Hostname (e.g. zap.local) or IP address of the device. Prefer the device\'s mDNS (.local) name when it broadcasts one — it survives DHCP lease changes. If you use a raw IP, reserve it for the device in your router\'s DHCP settings so it can\'t change.') + ' ' +
' ' +
- ((d.lua || '').indexOf('zap.lua') >= 0
+ (isZap
? 'This driver is the P1/HAN site meter by default. Prefer a native driver for inverters and batteries. Turn on a read below only when Zap is the only reader — a SolarEdge whose Modbus is closed, or an inverter on RS-485 that Zap already owns. Zap never writes.
' +
'' +
' attrs.match(new RegExp('\\b' + name + '="([^"]*)"'))?.[1];
@@ -50,6 +50,7 @@ function wizard(search = '', zonesFail = false) {
fetch(path, opts) {
requests.push({ path, opts });
if (zonesFail && path === '/api/prices/zones') return Promise.reject(new Error('offline'));
+ if (path === '/api/drivers/catalog') return Promise.resolve({ ok: true, json: async () => ({ entries: catalog }) });
return Promise.resolve({ ok: true, json: async () => path === '/api/prices/zones' ? { zones: [
{ country: 'Sweden', code: 'SE3', currency: 'SEK' },
{ country: 'Belgium', code: 'BE', currency: 'EUR' },
@@ -58,7 +59,7 @@ function wizard(search = '', zonesFail = false) {
};
sandbox.window = sandbox;
vm.runInNewContext(source, sandbox);
- return { elements, requests, go: sandbox.goStep, save: sandbox.saveConfig,
+ return { elements, requests, go: sandbox.goStep, save: sandbox.saveConfig, sandbox,
choose(id, value, event = 'change') { elements[id].value = value; elements[id].fire(event); },
posted: () => JSON.parse(requests.find(r => r.opts?.method === 'POST').opts.body),
visible: n => elements['step-' + n].classList.contains('visible'),
@@ -159,3 +160,25 @@ describe('setup electricity connection', () => {
assert.deepEqual(rig.posted().fuse, { phases: 3, voltage: 230, max_amps: 16 });
});
});
+
+describe('setup Zap driver', () => {
+ it('saves PV and battery reads as off, not missing', async () => {
+ const zap = { id: 'zap', filename: 'zap.lua', path: 'drivers/zap.lua', protocols: ['http'],
+ capabilities: ['meter', 'pv', 'battery'], read_only: true, connection_defaults: { host: '' } };
+ const rig = wizard('?step=2', false, [zap]);
+ rig.elements['fuse-amps'].value = '20';
+ rig.go(4);
+ await settled();
+ rig.choose('driver-select', '0');
+ rig.sandbox.onDriverSelected();
+ rig.go(5);
+ rig.elements['drv-name'].value = 'sourceful-zap';
+ rig.elements['drv-ip'].value = 'zap.local';
+ rig.sandbox.saveDriver();
+ rig.save();
+ const config = rig.posted().drivers[0].config;
+ assert.equal(config.host, 'zap.local');
+ assert.equal(config.read_pv, false);
+ assert.equal(config.read_battery, false);
+ });
+});
diff --git a/web/setup.js b/web/setup.js
index 1ffcfa594..ff6f46f51 100644
--- a/web/setup.js
+++ b/web/setup.js
@@ -316,6 +316,10 @@
}
}
+ function isZapCatalog(entry) {
+ return !!entry && (entry.filename === 'zap.lua' || entry.id === 'zap');
+ }
+
window.onDriverSelected = function () {
var sel = document.getElementById('driver-select');
var btn = document.getElementById('driver-next-btn');
@@ -345,7 +349,7 @@
btn.disabled = false;
var lines = [];
- if (selectedCatalog.filename === 'zap.lua' || selectedCatalog.id === 'zap') {
+ if (isZapCatalog(selectedCatalog)) {
lines.push('This driver is the P1/HAN site meter by default. Add inverters, batteries and chargers as their own devices in FTW when you can. If Zap is the only reader, turn on PV or battery ingest later under Settings → Devices.');
}
if (selectedCatalog.description) lines.push(selectedCatalog.description);
@@ -562,6 +566,14 @@
}
}
+ // Zap reads PV and battery only on opt-in. Save the off state, because
+ // Core treats a missing read_pv as PV that may still report.
+ if (isZapCatalog(selectedCatalog)) {
+ driver.config = driver.config || {};
+ driver.config.read_pv = false;
+ driver.config.read_battery = false;
+ }
+
// Persist per-driver secrets the prefill rendered (api_token, etc.)
// into driver.config.. Lives OUTSIDE the protocol branches so
// a future Modbus/MQTT driver that declares config_secrets still
diff --git a/web/setup.test.mjs b/web/setup.test.mjs
index bfddf70df..29e55f433 100644
--- a/web/setup.test.mjs
+++ b/web/setup.test.mjs
@@ -167,6 +167,12 @@ describe("setup wizard — read-only battery gateways", () => {
assert.match(DEVICES_JS, /drivers\.' \+ idx \+ '\.config\.read_battery/);
assert.match(DEVICES_JS, /Read PV from devices on this Zap/);
});
+
+ it("saves an unchecked Zap read as false instead of leaving the key out", () => {
+ // Core counts a missing read_pv as PV that may report and waits for it.
+ assert.match(DEVICES_JS, /if \(d\.config\.read_pv == null\) d\.config\.read_pv = false;/);
+ assert.match(DEVICES_JS, /if \(d\.config\.read_battery == null\) d\.config\.read_battery = false;/);
+ });
});
describe("price provider defaults", () => {