ESPSomfy-RTS/data-src/js/schema.js
cjkas 99c6019431 feat: web UI protocol 2 client layer, JS and CSS bundles (step 2a)
- data-src/js: ProtoSchema (schema from the sync, short keys and names,
  defaults, cached by hash), ProtoModel (rooms, shades, groups, repeaters,
  header and status from the sync and the events) and ProtoClient (/ws,
  hello, sync with next, op() as a Promise of its reply, events, feeds,
  reconnect). Nothing uses them yet: 2b moves the UI onto them.
- minify.py joins js/*.js and index.js into index.js, and the three CSS
  files into app.css, so a page load opens fewer connections.
- test/js: node --test on the three modules, replaying the recorded
  fixtures; a CI job runs it and the firmware build waits for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:00:23 +01:00

96 lines
4 KiB
JavaScript

// Socket protocol 2's schema (docs/PROTOCOL.md): the device's table of message
// types, fields, error codes and feeds, as the sync sends it. It turns the
// short keys of a frame into names and back. The minify step joins this file
// into index.js; Node loads it on its own for the tests (test/js).
class ProtoSchema {
constructor() {
this.hash = null;
this.types = {}; // number -> name
this.typeNumbers = {}; // name -> number
this.fields = {}; // key -> { name, kind, def }
this.keys = {}; // name -> key
this.errors = {};
this.errorNumbers = {};
this.feeds = {};
this.feedNumbers = {};
this.rows = { T: [], F: [], E: [], Q: [] };
}
// The schema's rows in a sync frame's payload: T types, F fields, E error
// codes, Q feeds. They may span several frames.
add(d) {
for (const [number, name] of d.T || []) {
this.types[number] = name;
this.typeNumbers[name] = number;
}
for (const [key, name, kind, def] of d.F || []) {
this.fields[key] = { name: name, kind: kind, def: def };
this.keys[name] = key;
}
for (const [number, name] of d.E || []) {
this.errors[number] = name;
this.errorNumbers[name] = number;
}
for (const [number, name] of d.Q || []) {
this.feeds[number] = name;
this.feedNumbers[name] = number;
}
for (const k of ['T', 'F', 'E', 'Q']) if (d[k]) this.rows[k].push(...d[k]);
}
get empty() { return Object.keys(this.fields).length === 0; }
// What a field left out of a frame is: its default.
defaultOf(name) {
const f = this.fields[this.keys[name]];
if (!f) return undefined;
switch (f.kind) {
case 'b': return f.def !== 0;
case 's': return '';
case 'l': return [];
case 'o': return {};
default: return f.def;
}
}
// Short keys to names, and 1/0 to true/false, all the way down. A field
// left out stays out; fill() puts the defaults in.
expand(value) {
if (Array.isArray(value)) return value.map((v) => this.expand(v));
if (value === null || typeof value !== 'object') return value;
const out = {};
for (const key of Object.keys(value)) {
const f = this.fields[key];
let v = value[key];
if (f && f.kind === 'b' && typeof v === 'number') v = v !== 0;
out[f ? f.name : key] = this.expand(v);
}
return out;
}
// Names to short keys, and booleans to 1/0, for an op's payload. A name
// the schema doesn't have is an error: the device would refuse it anyway.
pack(value) {
if (Array.isArray(value)) return value.map((v) => this.pack(v));
if (value === null || typeof value !== 'object') return typeof value === 'boolean' ? (value ? 1 : 0) : value;
const out = {};
for (const name of Object.keys(value)) {
if (typeof value[name] === 'undefined') continue;
const key = this.keys[name];
if (typeof key === 'undefined') throw new Error(`The device has no field ${name}`);
out[key] = this.pack(value[name]);
}
return out;
}
// Sets each of names that obj lacks to its default, as a frame leaves out
// a field at its default. Returns obj.
fill(obj, names) {
for (const name of names) if (typeof obj[name] === 'undefined') obj[name] = this.defaultOf(name);
return obj;
}
// What a cache keeps, and the schema back from it.
save() { return { hash: this.hash, rows: this.rows }; }
static load(saved) {
if (!saved || typeof saved.hash !== 'string' || !saved.rows) return null;
const s = new ProtoSchema();
s.add(saved.rows);
s.hash = saved.hash;
return s.empty ? null : s;
}
}
if (typeof module !== 'undefined' && module.exports) module.exports = { ProtoSchema };