diff --git a/code/espurna/relay.cpp b/code/espurna/relay.cpp index ac6bd7fe..61a5cf19 100644 --- a/code/espurna/relay.cpp +++ b/code/espurna/relay.cpp @@ -2364,7 +2364,7 @@ bool _relayWebSocketOnKeyCheck(espurna::StringView key, const JsonVariant&) { void _relayWebSocketUpdate(JsonObject& root) { espurna::web::ws::EnumerablePayload payload{root, STRING_VIEW("relayState")}; - payload(STRING_VIEW("states"), _relays.size(), { + payload(STRING_VIEW("values"), _relays.size(), { {STRING_VIEW("status"), [](JsonArray& out, size_t index) { out.add(_relays[index].target_status ? 1 : 0); }}, @@ -2385,7 +2385,7 @@ void _relayWebSocketSendRelays(JsonObject& root) { container[F("size")] = _relays.size(); container[F("start")] = 0; - config(STRING_VIEW("relays"), _relays.size(), + config(STRING_VIEW("values"), _relays.size(), espurna::relay::settings::query::IndexedSettings); } diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp index 146ab944..18d8c81e 100644 --- a/code/espurna/sensor.cpp +++ b/code/espurna/sensor.cpp @@ -3322,11 +3322,9 @@ void units(JsonObject& root) { const auto range = units::range(magnitude::get(index).type); for (auto it = range.begin(); it != range.end(); ++it) { - JsonArray& unit = units.createNestedArray(); + JsonArray& unit = supported.createNestedArray(); unit.add(static_cast(*it)); // raw id unit.add(units::name(*it)); // as string - - supported.add(unit); } } } diff --git a/code/html/src/curtain.mjs b/code/html/src/curtain.mjs index c87c5585..ee45a078 100644 --- a/code/html/src/curtain.mjs +++ b/code/html/src/curtain.mjs @@ -11,7 +11,7 @@ function buttonHandler(event) { return; } - if (!(event.target instanceof HTMLInputElement)) { + if (!(event.target instanceof HTMLButtonElement)) { return; } @@ -39,7 +39,7 @@ function buttonHandler(event) { * @param {number} button */ function styleButtons(moving, button) { - const elems = /** @type {NodeListOf} */ + const elems = /** @type {NodeListOf} */ (document.querySelectorAll("curtain-button")); if (!moving || (0 === button)) { if (!moving) { @@ -76,7 +76,7 @@ function styleButtons(moving, button) { /** @param {Event} event */ function positionHandler(event) { - if (!(event.target instanceof HTMLInputElement)) { + if (!(event.target instanceof HTMLButtonElement)) { return; } diff --git a/code/html/src/index.mjs b/code/html/src/index.mjs index dba4dc6a..96223f77 100644 --- a/code/html/src/index.mjs +++ b/code/html/src/index.mjs @@ -50,6 +50,7 @@ import { init as initSchedule } from './schedule.mjs'; import { init as initSensor } from './sensor.mjs'; import { init as initThermostat } from './thermostat.mjs'; import { init as initThingspeak } from './thingspeak.mjs'; +import { init as initLocal } from './local.mjs'; /** @type {number | null} */ let KeepTime = null; @@ -523,17 +524,14 @@ function init() { initCurtain(); } - // don't autoconnect w/ localhost or file:// if (MODULE_LOCAL) { - updateVariables({ - webMode: 0, - now: "2024-01-01T00:00:00+01:00", - }); + initLocal(); KeepTime = window.setInterval(keepTime, 1000); modulesVisibleAll(); return; } + // don't autoconnect w/ localhost or file:// connect(onJsonPayload); } diff --git a/code/html/src/local.mjs b/code/html/src/local.mjs new file mode 100644 index 00000000..70bb1cdf --- /dev/null +++ b/code/html/src/local.mjs @@ -0,0 +1,300 @@ +import { updateVariables } from './settings.mjs'; + +export function init() { + updateVariables({ + webMode: 0, + useWhite: false, + useCCT: false, + useColor: true, + useRGB: true, + now: "2024-01-01T00:00:00+01:00", + light: { + channels: ['r', 'g', 'b'], + }, + gpioConfig: { + types: [['hardware', 1]], + hardware: [ + 0, + 1, + 2, + 3, + 4, + 5, + 9, + 10, + 12, + 13, + 14, + 15, + 16, + ], + }, + curtainType: 0, + curtainBoot: 0, + relayConfig: { + size: 5, + start: 0, + schema: [ + 'relayName', + 'relayProv', + 'relayType', + 'relayGpioType', + 'relayGpio', + 'relayResetGpio', + 'relayBoot', + 'relayDelayOn', + 'relayDelayOff', + 'relayTime', + 'relayPulse', + 'relayTopicPub', + 'relayTopicSub', + 'relayTopicMode', + 'relayMqttDisc', + ], + relays: [ + ['111', 'gpio', 0, 1, 4, 153, 0, 0, 0, 0, 0, '', '', 0, 0], + ['222', 'gpio', 0, 1, 5, 153, 0, 0, 0, 0, 0, '', '', 0, 0], + ['333', 'gpio', 0, 1, 12, 153, 0, 0, 0, 0, 0, '', '', 0, 0], + ['444', 'gpio', 0, 1, 13, 153, 0, 0, 0, 0, 0, '', '', 0, 0], + ['555', 'gpio', 0, 1, 14, 153, 0, 0, 0, 0, 0, '', '', 0, 0], + ], + }, + ledConfig: { + schema: [ + 'ledGpio', + 'ledInv', + 'ledMode', + 'ledRelay', + ], + leds: [ + [15, 0, 'relay-inverse', 0], + [16, 0, 'relay', 1], + ], + }, + rfm69Topic: 'foo', + rfm69: { + packets: 123, + nodes: 456, + schema: [ + 'rfm69Node', + 'rfm69Key', + 'rfm69Topic', + ], + mapping: [ + [1, 'one', 'two'], + [2, 'three', 'four'], + [3, 'five', 'six'], + [4, 'seven', 'eight'], + [5, 'nine', 'ten'], + ], + }, + rfbRepeat: 25, + rfbCount: 5, + rfbRX: 1, + rfbTX: 3, + }); + updateVariables({ + hostname: 'localhost', + desc: 'local.mjs variables', + ssid: 'ESPURNA-12345', + bssid: '11:22:33:aa:ee', + channel: 13, + rssi: -54, + staip: '10.10.10.5', + apip: '192.168.4.1', + app_name: 'ESPurna', + app_version: '9.9.9-dev', + app_build: '9999-12-12 23:59:59', + sketch_size: '555555', + free_size: 493021, + uptime: '5d 30m 13s', + manufacturer: 'WEB', + device: navigator.userAgent, + chipid: window.name, + sdk: 'WEB', + core: 'WEB', + heap: 999999, + loadaverage: 99, + vcc: '3.3', + mqttStatus: true, + ntpStatus: true, + dczRelays: [ + 0, + 0, + 0, + 0, + 0, + ], + tspkRelays: [ + 0, + 0, + 0, + 0, + 0, + ], + light: { + rgb: "#aaffaa", + brightness: 100, + state: true, + }, + curtainState: { + get: 20, + set: 50, + button: 0, + moving: 0, + type: 2, + }, + rfb: { + start: 0, + codes: [ + ['aaaaaa', 'bbbbbb'], + ['cccccc', 'dddddd'], + ['ffffff', 'ffffff'], + ['333333', '000000'], + ['222222', '111111'], + ], + }, + rpnRules: [ + '2 2 +', + '5 5 *', + 'yield', + 'rssi not', + '$value $value + "foo/bar" mqtt.send' + ], + rpnTopics: { + schema: [ + 'rpnName', + 'rpnTopic', + ], + topics: [ + ['value', 'hello/world'], + ['other', 'world/hello'], + ], + }, + schConfig: { + schedules: [ + [1, 0, 'relay 0 1', '05,10:00'], + [1, 0, 'relay 1 2', '05:00'], + [1, 0, 'relay 2 0\nrelay 0 0', '10:00'], + ], + schema: [ + 'schType', + 'schRestore', + 'schAction', + 'schTime', + ], + max: 4, + }, + 'magnitudes-init': { + types: { + schema: [ + 'type', + 'prefix', + 'name', + ], + values: [ + [0, 'foo', 'Foo'], + [1, 'bar', 'Bar'], + [2, 'baz', 'Baz'], + ], + }, + errors: { + schema: [ + 'type', + 'name', + ], + values: [ + [0, 'OK'], + [1, 'FAIL'], + [99, 'OUT_OF_RANGE'], + ], + }, + units: [ + [[1, 'C']], + [[2, 'F']], + [[3, 'K']], + ], + }, + thermostatMode: true, + thermostatTmpUnits: 'C', + thermostatOperationMode: 'local window', + remoteTmp: '23', + wifiConfig: { + schema: [ + 'ssid', + 'pass', + 'ip', + 'gw', + 'mask', + 'dns', + 'bssid', + 'chan', + ], + networks: [ + ['ESPURNA-12345', 'fibonacci', '', '', '', '', '', ''], + ], + max: 5, + }, + }); + + updateVariables({ + 'magnitudes-list': { + schema: [ + 'index_global', + 'type', + 'description', + 'units', + ], + values: [ + [0, 0, "Foo measurements", 1], + [0, 1, "Bar simulation", 2], + [0, 2, "Baz calculation", 2], + ], + }, + }); + + updateVariables({ + magnitudes: { + schema: [ + 'value', + 'units', + 'error', + ], + values: [ + [23, 1, 0], + [98, 2, 0], + [296, 3, 99], + ], + }, + relayState: { + schema: [ + 'status', + 'lock', + ], + relays: [ + [0, 0], + [1, 2], + [0, 1], + [1, 0], + [1, 0], + ], + }, + }); + + for (let n = 100; n < 200; n += 15) { + updateVariables({ + rfm69: { + message: [ + n, + 456, + 789, + 'one', + 'eno', + -127, + 0, + 0, + ], + }, + }); + } +} diff --git a/code/html/src/ota.mjs b/code/html/src/ota.mjs index 701cd77d..61928896 100644 --- a/code/html/src/ota.mjs +++ b/code/html/src/ota.mjs @@ -4,6 +4,18 @@ import { variableListeners } from './settings.mjs'; let __free_size = 0; +/** @param {function(HTMLInputElement): void} callback */ +function withUpgrade(callback) { + callback(/** @type {!HTMLInputElement} */ + (document.querySelector("input[name=upgrade]"))); +} + +/** @param {function(HTMLProgressElement): void} callback */ +function withProgress(callback) { + callback(/** @type {!HTMLProgressElement} */ + (document.querySelector("progress#upgrade-progress"))); +} + /** * @param {number} flash_mode * @returns string @@ -64,54 +76,52 @@ function onButtonClick(event) { const urls = connectionUrls(); if (!urls) { + alert("Not connected"); return; } - const upgrade = document.querySelector("input[name='upgrade']"); - if (!(upgrade instanceof HTMLInputElement)) { - return; - } + withUpgrade((upgrade) => { + const files = upgrade.files; + if (!files) { + alert("No file selected"); + return; + } - const files = upgrade.files; - if (!files) { - return; - } + const data = new FormData(); + data.append("upgrade", files[0], files[0].name); - const data = new FormData(); - data.append("upgrade", files[0], files[0].name); + const xhr = new XMLHttpRequest(); - const xhr = new XMLHttpRequest(); + xhr.addEventListener("error", notifyValueError, false); + xhr.addEventListener("abort", notifyValueError, false); - xhr.addEventListener("error", notifyValueError, false); - xhr.addEventListener("abort", notifyValueError, false); - - xhr.addEventListener("load", - () => { - if ("OK" === xhr.responseText) { - alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds"); - } else { - alert(`ERROR while attempting OTA upgrade - ${xhr.status.toString()} ${xhr.statusText}, ${xhr.responseText}`); - } - }, false); - - const progress = document.querySelector("progress#upgrade-progress"); - if (progress instanceof HTMLProgressElement) { xhr.addEventListener("load", () => { - progress.style.display = "none"; - }); - xhr.upload.addEventListener("progress", - (event) => { - progress.style.display = "inherit"; - if (event.lengthComputable) { - progress.value = event.loaded; - progress.max = event.total; + if ("OK" === xhr.responseText) { + alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds"); + } else { + alert(`ERROR while attempting OTA upgrade - ${xhr.status.toString()} ${xhr.statusText}, ${xhr.responseText}`); } }, false); - } - xhr.open("POST", urls.upgrade.href); - xhr.send(data); + withProgress((progress) => { + xhr.addEventListener("load", + () => { + progress.style.display = "none"; + }); + xhr.upload.addEventListener("progress", + (event) => { + progress.style.display = "inherit"; + if (event.lengthComputable) { + progress.value = event.loaded; + progress.max = event.total; + } + }, false); + }); + + xhr.open("POST", urls.upgrade.href); + xhr.send(data); + }); } /** @@ -129,7 +139,7 @@ async function onFileChanged(event) { event.preventDefault(); const button = document.querySelector(".button-upgrade"); - if (!(button instanceof HTMLInputElement)) { + if (!(button instanceof HTMLButtonElement)) { return; } @@ -145,7 +155,7 @@ async function onFileChanged(event) { const file = event.target.files[0]; - const filename = document.querySelector("input[name='filename']"); + const filename = document.querySelector("input[name=filename]"); if (filename instanceof HTMLInputElement) { filename.value = file.name; } @@ -187,20 +197,20 @@ function listeners() { export function init() { variableListeners(listeners()); - const upgrade = document.querySelector("input[name='upgrade']"); + const [upgrade] = document.querySelectorAll("input[name=upgrade]"); if (!(upgrade instanceof HTMLInputElement)) { return; } - document.querySelector(".button-upgrade-browse") - ?.addEventListener("click", () => { - upgrade.click(); - }); + const [browse] = document.querySelectorAll(".button-upgrade-browse") + browse.addEventListener("click", () => { + upgrade.click(); + }); upgrade.addEventListener("change", onFileChanged); const button = document.querySelector(".button-upgrade"); - if (!(button instanceof HTMLInputElement)) { + if (!(button instanceof HTMLButtonElement)) { return; } diff --git a/code/html/src/panel-admin.html b/code/html/src/panel-admin.html index 16531706..1378783c 100644 --- a/code/html/src/panel-admin.html +++ b/code/html/src/panel-admin.html @@ -93,7 +93,7 @@ - + diff --git a/code/html/src/relay.mjs b/code/html/src/relay.mjs index 0bce70c2..be7f6e98 100644 --- a/code/html/src/relay.mjs +++ b/code/html/src/relay.mjs @@ -149,7 +149,7 @@ export function createNodeList(id, values, keyPrefix) { (label, input) => { const enumerables = getEnumerables("relay"); label.textContent = - (enumerables) + (enumerables[index]) ? enumerables[index].name : `Switch #${index}`; @@ -164,10 +164,10 @@ export function createNodeList(id, values, keyPrefix) { function listeners() { return { "relayConfig": (_, value) => { - updateFromConfig(value.relays, value.schema); + updateFromConfig(value.values, value.schema); }, "relayState": (_, value) => { - updateFromState(value.relays, value.schema); + updateFromState(value.values, value.schema); }, }; } diff --git a/code/html/src/rfbridge.mjs b/code/html/src/rfbridge.mjs index c9e2c249..0637df06 100644 --- a/code/html/src/rfbridge.mjs +++ b/code/html/src/rfbridge.mjs @@ -49,19 +49,19 @@ function addNode() { const id = container.childElementCount.toString(); const line = loadConfigTemplate("rfb-node"); - line.querySelectorAll("span").forEach((span) => { - span.textContent = id; - }); - line.querySelectorAll("input").forEach((input) => { - input.dataset["id"] = id; - input.setAttribute("id", `${input.name}${id}`); - }); - for (let action of ["learn", "forget"]) { - for (let button of line.querySelectorAll(`.button-rfb-${action}`)) { + line.querySelectorAll("input") + .forEach((input) => { + input.dataset["id"] = id; + input.setAttribute("id", `${input.name}${id}`); + }); + + ["learn", "forget"] + .map((x) => line.querySelector(`.button-rfb-${x}`)) + .filter((x) => x instanceof HTMLButtonElement) + .forEach((button) => { button.addEventListener("click", onButtonPress); - } - } + }); mergeTemplate(container, line); } diff --git a/code/html/src/rfm69.mjs b/code/html/src/rfm69.mjs index 8d0bad05..4bbd3d07 100644 --- a/code/html/src/rfm69.mjs +++ b/code/html/src/rfm69.mjs @@ -10,14 +10,21 @@ import { sendAction } from './connection.mjs'; const Filters = new Map(); /** @param {function(HTMLTableElement): void} callback */ -function withMessages(callback) { +function withTable(callback) { callback(/** @type {!HTMLTableElement} */ (document.getElementById("rfm69-messages"))); } +/** @param {function(HTMLTableSectionElement): void} callback */ +function withMessages(callback) { + withTable((elem) => { + callback(elem.tBodies[0]); + }); +} + /** @param {[number, number, number, string, string, number, number, number]} message */ function addMessage(message) { - withMessages((elem) => { + withTable((elem) => { const timestamp = (new Date()) .toLocaleTimeString("en-US", {hour12: false}); diff --git a/code/html/src/sensor.mjs b/code/html/src/sensor.mjs index 00b21d84..3d33fd49 100644 --- a/code/html/src/sensor.mjs +++ b/code/html/src/sensor.mjs @@ -134,7 +134,7 @@ function initMagnitudes(types, errors, units) { }); /** @type {SupportedUnits[][]} */ - (units.values).forEach((value, id) => { + (units).forEach((value, id) => { Magnitudes.units.supported.set(id, value); value.forEach(([type, name]) => { Magnitudes.units.names.set(type, name); @@ -483,15 +483,16 @@ function updateMagnitudes(values, schema) { props.units = magnitude.units; } + const units = + Magnitudes.units.names.get(props.units) ?? ""; + if (typeof magnitude.error === "number" && 0 !== magnitude.error) { input.value = Magnitudes.errors.get(magnitude.error) ?? "Unknown error"; - } else if (typeof magnitude.value === "number") { - const units = - Magnitudes.units.names.get(props.units) ?? ""; + } else if (typeof magnitude.value === "string") { input.value = `${magnitude.value}${units}`; } else { - input.value = "?"; + input.value = magnitude.value?.toString() ?? "?"; } }); } diff --git a/code/html/src/settings.mjs b/code/html/src/settings.mjs index b840f5b5..f3e56f8c 100644 --- a/code/html/src/settings.mjs +++ b/code/html/src/settings.mjs @@ -137,12 +137,6 @@ function isIgnoredElement(elem) { return elem.dataset["settingsIgnore"] !== undefined; } -/** - * @param {HTMLElement} elem - */ -function isInputOrSelect(elem) { - return (elem instanceof HTMLInputElement) || (elem instanceof HTMLSelectElement); -} /** * @param {HTMLElement} group @@ -762,6 +756,10 @@ export function setSelectValue(select, value) { export function setOriginalsFromValues(elems) { for (let elem of elems) { if (elem instanceof HTMLInputElement) { + if (elem.readOnly) { + continue; + } + if (elem.type === "checkbox") { elem.dataset["original"] = elem.checked.toString(); } else { @@ -1020,11 +1018,13 @@ export function checkAndSetElementChanged(elem) { */ export function onElementChange(event) { const target = event.target; - if (!(target instanceof HTMLElement)) { + if (!(target instanceof HTMLInputElement) + && !(target instanceof HTMLSelectElement)) + { return; } - if (!isInputOrSelect(target)) { + if (target instanceof HTMLInputElement && target.readOnly) { return; } diff --git a/code/html/src/template-rfb.html b/code/html/src/template-rfb.html index 51e7703b..eaeeb7b8 100644 --- a/code/html/src/template-rfb.html +++ b/code/html/src/template-rfb.html @@ -1,6 +1,6 @@