mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
- The field is hasOtaSlot, like hasEthernet and the other "has" bools. - The web UI shows the device's reason for any failed upload, not only for /restore, so it shows "no OTA slot" from a page that missed the header. The update routes' 500 replies give Update.errorString() instead of an empty reason. - Native: the writer leaves hasOtaSlot out when true and writes "os":0. - e2e: header() next to uptime(); the test skips on firmware without the field instead of failing, and matches the reason loosely. The README says what the test sends. - PROTOCOL.md and the comments name only the builds that exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
138 lines
5.6 KiB
JavaScript
138 lines
5.6 KiB
JavaScript
// The web UI's ProtoModel (data-src/js/model.js): the recorded sync, then
|
|
// events.
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { ProtoModel } = require('../../data-src/js/model.js');
|
|
const { recordedSchema, deviceSections, recordedItems } = require('./helpers.js');
|
|
|
|
function synced() {
|
|
const schema = recordedSchema();
|
|
const model = new ProtoModel(schema);
|
|
model.beginSync();
|
|
for (const d of deviceSections(schema)) model.applySync(d);
|
|
model.endSync();
|
|
return model;
|
|
}
|
|
|
|
test('the sync fills every room, shade and group', () => {
|
|
const m = synced();
|
|
assert.equal(m.rooms.size, recordedItems('R').length);
|
|
assert.equal(m.shades.size, recordedItems('S').length);
|
|
assert.equal(m.groups.size, recordedItems('G').length);
|
|
assert.equal(m.header.model, 'ESPSomfyRTS');
|
|
assert.equal(m.header.maxShades, 32);
|
|
assert.ok(m.status.freeHeap > 0);
|
|
});
|
|
|
|
test('a field the sync left out is its default', () => {
|
|
const m = synced();
|
|
const raw = recordedItems('S').find((s) => typeof s.m === 'undefined');
|
|
const shade = m.shades.get(raw.s);
|
|
// Left out because they were at their default.
|
|
assert.equal(shade.myPos, -1);
|
|
assert.equal(shade.stepSize, raw.z === undefined ? 100 : raw.z);
|
|
assert.equal(typeof shade.paired, 'boolean');
|
|
assert.ok(Array.isArray(shade.linkedRemotes));
|
|
for (const r of shade.linkedRemotes) assert.equal(typeof r.lastRollingCode, 'number');
|
|
for (const g of m.groupList()) assert.ok(Array.isArray(g.linkedShades));
|
|
});
|
|
|
|
test('the device has an OTA slot unless it says it has none', () => {
|
|
// The recording is from firmware before the field.
|
|
assert.equal(synced().header.hasOtaSlot, undefined);
|
|
const schema = recordedSchema();
|
|
schema.add({ F: [['os', 'hasOtaSlot', 'b', 1]] });
|
|
const header = deviceSections(schema).find((d) => d.header).header;
|
|
const m = new ProtoModel(schema);
|
|
m.applySync({ header: header });
|
|
assert.equal(m.header.hasOtaSlot, true);
|
|
m.applySync({ header: schema.expand(Object.assign({ os: 0 }, schema.pack(header))) });
|
|
assert.equal(m.header.hasOtaSlot, false);
|
|
});
|
|
|
|
test('the lists come in their sort order', () => {
|
|
const m = synced();
|
|
const orders = m.shadeList().map((s) => s.sortOrder);
|
|
assert.deepEqual(orders, [...orders].sort((a, b) => a - b));
|
|
});
|
|
|
|
test('a state event changes only the state, and a left-out field is its default', () => {
|
|
const m = synced();
|
|
const shade = m.shadeList()[0];
|
|
const name = shade.name;
|
|
// A shade at rest at the top: position 0 is left out.
|
|
assert.ok(m.applyEvent('shadeState', { shadeId: shade.shadeId, target: 40, direction: 1 }));
|
|
assert.equal(m.shades.get(shade.shadeId).position, 0);
|
|
assert.equal(m.shades.get(shade.shadeId).target, 40);
|
|
assert.equal(m.shades.get(shade.shadeId).name, name);
|
|
// Of a shade the client doesn't have yet: nothing to change.
|
|
assert.equal(m.applyEvent('shadeState', { shadeId: 250, position: 5 }), false);
|
|
});
|
|
|
|
test('a whole item replaces what was there', () => {
|
|
const m = synced();
|
|
const shade = m.shadeList()[0];
|
|
m.applyEvent('shade', { shadeId: shade.shadeId, name: 'Renamed' });
|
|
assert.equal(m.shades.get(shade.shadeId).name, 'Renamed');
|
|
assert.equal(m.shades.get(shade.shadeId).upTime, 0);
|
|
m.applyEvent('room', { roomId: 9, name: 'New' });
|
|
assert.equal(m.rooms.get(9).sortOrder, 0);
|
|
});
|
|
|
|
test('removals, orders and repeaters', () => {
|
|
const m = synced();
|
|
const changes = [];
|
|
m.onChange((kind, id) => changes.push([kind, id]));
|
|
const [first, second] = m.shadeList();
|
|
assert.ok(m.applyEvent('shadeRemoved', { shadeId: first.shadeId }));
|
|
assert.equal(m.shades.has(first.shadeId), false);
|
|
// An id it doesn't have is ignored.
|
|
assert.equal(m.applyEvent('roomRemoved', { roomId: 250 }), false);
|
|
m.applyEvent('shadeOrder', { ids: [second.shadeId] });
|
|
assert.equal(m.shades.get(second.shadeId).sortOrder, 0);
|
|
m.applyEvent('repeaters', { repeaters: [7, 8] });
|
|
assert.deepEqual(m.repeaters, [7, 8]);
|
|
// An order says which list it is.
|
|
assert.deepEqual(changes.map((c) => c[0]), ['shadeRemoved', 'shadeOrder', 'repeaters']);
|
|
});
|
|
|
|
test('a sync without repeaters clears them, as the device leaves the list out when it has none', () => {
|
|
const m = synced();
|
|
m.applyEvent('repeaters', { repeaters: [7] });
|
|
m.beginSync();
|
|
for (const d of deviceSections(m.schema)) m.applySync(Object.assign({}, d, { repeaters: undefined }));
|
|
m.endSync();
|
|
assert.deepEqual(m.repeaters, []);
|
|
});
|
|
|
|
test('a sync that is called off removes nothing', () => {
|
|
const m = synced();
|
|
const shades = m.shades.size;
|
|
m.beginSync();
|
|
m.cancelSync();
|
|
m.endSync();
|
|
assert.equal(m.shades.size, shades);
|
|
});
|
|
|
|
test('a sync drops what it no longer lists, and keeps what an event added during it', () => {
|
|
const m = synced();
|
|
const schema = m.schema;
|
|
m.applyEvent('room', { roomId: 99, name: 'Gone' });
|
|
m.beginSync();
|
|
m.applyEvent('room', { roomId: 98, name: 'Added during the sync' });
|
|
for (const d of deviceSections(schema)) m.applySync(d);
|
|
m.endSync();
|
|
assert.equal(m.rooms.has(99), false);
|
|
assert.equal(m.rooms.has(98), true);
|
|
});
|
|
|
|
test('status events', () => {
|
|
const m = synced();
|
|
m.applyEvent('memory', { freeHeap: 1000, uptime: 5 });
|
|
assert.equal(m.status.freeHeap, 1000);
|
|
assert.equal(m.status.loopRate, 0);
|
|
m.applyEvent('wifi', { strength: -60, channel: 6 });
|
|
assert.equal(m.status.ssid, '');
|
|
assert.equal(m.status.strength, -60);
|
|
assert.equal(m.applyEvent('frame', { remoteAddress: 1 }), false);
|
|
});
|