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
5 changes: 5 additions & 0 deletions .changeset/zap-save-read-pv-off.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 9 additions & 1 deletion web/settings/tabs/devices.js
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,14 @@
'</fieldset>';
}
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
Expand All @@ -1219,7 +1227,7 @@
html += '<fieldset><legend>HTTP</legend>' +
'<label>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.') + '</label>' +
'<input type="text" data-path="drivers.' + idx + '.config.host" value="' + escHtml(lcfg.host || '') + '" placeholder="zap.local">' +
((d.lua || '').indexOf('zap.lua') >= 0
(isZap
? '<p class="zap-p1-note" style="margin:8px 0 0;font-size:0.82rem;color:var(--text-dim);line-height:1.45">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.</p>' +
'<label class="drv-read-pv" style="margin-top:8px;display:flex;align-items:center;gap:6px;font-weight:normal">' +
'<input type="checkbox" data-checkbox-path="drivers.' + idx + '.config.read_pv"' +
Expand Down
27 changes: 25 additions & 2 deletions web/setup-connection.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const source = readFileSync(new URL('./setup.js', import.meta.url), 'utf8');

// Run the wizard's real navigation and save handlers. Initial field values
// come from the HTML so a changed default also changes the posted config.
function wizard(search = '', zonesFail = false) {
function wizard(search = '', zonesFail = false, catalog = []) {
const elements = {}, requests = [];
function element(attrs = '') {
const attr = name => attrs.match(new RegExp('\\b' + name + '="([^"]*)"'))?.[1];
Expand Down Expand Up @@ -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' },
Expand All @@ -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'),
Expand Down Expand Up @@ -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);
});
});
14 changes: 13 additions & 1 deletion web/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.<key>. Lives OUTSIDE the protocol branches so
// a future Modbus/MQTT driver that declares config_secrets still
Expand Down
6 changes: 6 additions & 0 deletions web/setup.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading