mirror of
https://github.com/xoseperez/espurna.git
synced 2026-03-05 16:04:19 +01:00
webui: more typings, clean-up sensor units init
This commit is contained in:
@@ -2,12 +2,21 @@ import { randomString } from './core.mjs';
|
||||
import { setChangedElement } from './settings.mjs';
|
||||
|
||||
function randomApiKey() {
|
||||
const elem = document.forms["form-admin"].elements.apiKey;
|
||||
const form = document.forms.namedItem("form-admin");
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elem = form.elements.namedItem("apiKey");
|
||||
if (!(elem instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
elem.value = randomString(16, {hex: true});
|
||||
setChangedElement(elem);
|
||||
}
|
||||
|
||||
export function init() {
|
||||
document.querySelector(".button-apikey")
|
||||
.addEventListener("click", randomApiKey);
|
||||
?.addEventListener("click", randomApiKey);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export function onPanelTargetClick(event) {
|
||||
* @param {number} length
|
||||
* @param {RandomStringOptions} options
|
||||
*/
|
||||
export function randomString(length, {hex = false, lowercase = true, numbers = true, special = false, uppercase = true} = {}) {
|
||||
export function randomString(length, {hex = false, lowercase = false, numbers = false, special = false, uppercase = false} = {}) {
|
||||
let mask = "";
|
||||
if (lowercase || hex) { mask += "abcdef"; }
|
||||
if (lowercase) { mask += "ghijklmnopqrstuvwxyz"; }
|
||||
|
||||
@@ -2,20 +2,19 @@ import { sendAction } from './connection.mjs';
|
||||
import { loadConfigTemplate, mergeTemplate } from './template.mjs';
|
||||
import { addSimpleEnumerables, variableListeners } from './settings.mjs';
|
||||
|
||||
function listeners() {
|
||||
return {
|
||||
"curtainState": (_, value) => {
|
||||
initCurtain();
|
||||
updateCurtain(value);
|
||||
},
|
||||
};
|
||||
}
|
||||
const BACKGROUND_MOVING = "rgb(192, 0, 0)";
|
||||
const BACKGROUND_STOPPED = "rgb(64, 184, 221)";
|
||||
|
||||
/** @param {Event} event */
|
||||
function buttonHandler(event) {
|
||||
if (event.type !== "click") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(event.target instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
let code = -1;
|
||||
@@ -35,47 +34,123 @@ function buttonHandler(event) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} moving
|
||||
* @param {number} button
|
||||
*/
|
||||
function styleButtons(moving, button) {
|
||||
const elems = /** @type {NodeListOf<HTMLInputElement>} */
|
||||
(document.querySelectorAll("curtain-button"));
|
||||
if (!moving || (0 === button)) {
|
||||
if (!moving) {
|
||||
elems.forEach((elem) => {
|
||||
elem.style.background = BACKGROUND_STOPPED;
|
||||
});
|
||||
}
|
||||
if (0 === button) {
|
||||
elems.forEach((elem) => {
|
||||
if (elem.classList.contains("button-curtain-pause")) {
|
||||
elem.style.background = BACKGROUND_MOVING;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
elems.forEach((elem) => {
|
||||
if (elem.classList.contains("button-curtain-open")) {
|
||||
elem.style.background =
|
||||
(1 === button) ? BACKGROUND_MOVING :
|
||||
(2 === button) ? BACKGROUND_STOPPED :
|
||||
BACKGROUND_STOPPED;
|
||||
} else if (elem.classList.contains("button-curtain-close")) {
|
||||
elem.style.background =
|
||||
(1 === button) ? BACKGROUND_STOPPED :
|
||||
(2 === button) ? BACKGROUND_MOVING :
|
||||
BACKGROUND_STOPPED;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/** @param {Event} event */
|
||||
function positionHandler(event) {
|
||||
if (!(event.target instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendAction("curtainAction", {position: event.target.value});
|
||||
}
|
||||
|
||||
//Create the controls for one curtain. It is called when curtain is updated (so created the first time)
|
||||
//Let this there as we plan to have more than one curtain per switch
|
||||
function initCurtain() {
|
||||
let container = document.getElementById("curtains");
|
||||
if (container.childElementCount > 0) {
|
||||
const container = document.getElementById("curtains");
|
||||
if (!container || container.childElementCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add and init curtain template, prepare multi switches
|
||||
let line = loadConfigTemplate("curtain-control");
|
||||
line.querySelector(".button-curtain-open")
|
||||
.addEventListener("click", buttonHandler);
|
||||
line.querySelector(".button-curtain-pause")
|
||||
.addEventListener("click", buttonHandler);
|
||||
line.querySelector(".button-curtain-close")
|
||||
.addEventListener("click", buttonHandler);
|
||||
const line = loadConfigTemplate("curtain-control");
|
||||
["open", "pause", "close"]
|
||||
.forEach((name) => {
|
||||
line.querySelector(`.button-curtain-${name}`)
|
||||
?.addEventListener("click", buttonHandler);
|
||||
});
|
||||
|
||||
mergeTemplate(container, line);
|
||||
|
||||
// simple position slider
|
||||
document.getElementById("curtainSet")
|
||||
.addEventListener("change", positionHandler);
|
||||
?.addEventListener("change", positionHandler);
|
||||
|
||||
addSimpleEnumerables("curtain", "Curtain", 1);
|
||||
}
|
||||
|
||||
function setBackground(a, b) {
|
||||
let elem = document.getElementById("curtainGetPicture");
|
||||
elem.style.background = `linear-gradient(${a}, black ${b}%, #a0d6ff ${b}%)`;
|
||||
/** @param {function(HTMLInputElement): void} callback */
|
||||
function withSet(callback) {
|
||||
const elem = document.getElementById("curtain-set");
|
||||
if (elem instanceof HTMLInputElement) {
|
||||
callback(elem);
|
||||
}
|
||||
}
|
||||
|
||||
function setBackgroundTwoSides(a, b) {
|
||||
let elem = document.getElementById("curtainGetPicture");
|
||||
elem.style.background = `linear-gradient(90deg, black ${a}%, #a0d6ff ${a}% ${b}%, black ${b}%)`;
|
||||
/** @param {function(HTMLElement): void} callback */
|
||||
function withPicture(callback) {
|
||||
const elem = document.getElementById("curtain-picture");
|
||||
if (elem instanceof HTMLElement) {
|
||||
callback(elem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} side_or_corner
|
||||
* @param {number} angle
|
||||
*/
|
||||
function setBackground(side_or_corner, angle) {
|
||||
withPicture((elem) => {
|
||||
elem.style.background =
|
||||
`linear-gradient(${side_or_corner}, black ${angle}%, #a0d6ff ${angle}%)`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} angle
|
||||
* @param {number} hint_angle
|
||||
*/
|
||||
function setBackgroundTwoSides(angle, hint_angle) {
|
||||
withPicture((elem) => {
|
||||
elem.style.background =
|
||||
`linear-gradient(90deg, black ${angle}%, #a0d6ff ${angle}% ${hint_angle}%, black ${hint_angle}%)`;
|
||||
});
|
||||
}
|
||||
|
||||
/** @typedef {{get: number, set: number, button: number, moving: boolean, type: string}} CurtainValue */
|
||||
|
||||
/** @param {CurtainValue} value */
|
||||
function updateCurtain(value) {
|
||||
switch(value.type) {
|
||||
switch (value.type) {
|
||||
case '1': //One side left to right
|
||||
setBackground('90deg', value.get);
|
||||
break;
|
||||
@@ -83,7 +158,7 @@ function updateCurtain(value) {
|
||||
setBackground('270deg', value.get);
|
||||
break;
|
||||
case '3': //Two sides
|
||||
setBackgroundTwoSides(value.get / 2, (100 - value.get/2));
|
||||
setBackgroundTwoSides(value.get / 2, (100 - value.get / 2));
|
||||
break;
|
||||
case '0': //Roller
|
||||
default:
|
||||
@@ -91,29 +166,23 @@ function updateCurtain(value) {
|
||||
break;
|
||||
}
|
||||
|
||||
let set = document.getElementById("curtainSet");
|
||||
set.value = value.set;
|
||||
withSet((elem) => {
|
||||
elem.value = value.set.toString();
|
||||
});
|
||||
|
||||
const backgroundMoving = 'rgb(192, 0, 0)';
|
||||
const backgroundStopped = 'rgb(64, 184, 221)';
|
||||
styleButtons(value.moving, value.button);
|
||||
}
|
||||
|
||||
if (!value.moving) {
|
||||
let button = document.querySelector("button.curtain-button");
|
||||
button.style.background = backgroundStopped;
|
||||
} else if (!value.button) {
|
||||
let pause = document.querySelector("button.curtain-pause");
|
||||
pause.style.background = backgroundMoving;
|
||||
} else {
|
||||
let open = document.querySelector("button.button-curtain-open");
|
||||
let close = document.querySelector("button.button-curtain-close");
|
||||
if (value.button === 1) {
|
||||
open.style.background = backgroundMoving;
|
||||
close.style.background = backgroundStopped;
|
||||
} else if (value.button === 2) {
|
||||
open.style.background = backgroundStopped;
|
||||
close.style.background = backgroundMoving;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"curtainState": (_, value) => {
|
||||
initCurtain();
|
||||
updateCurtain(value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function init() {
|
||||
|
||||
@@ -77,11 +77,7 @@ class CmdOutputBase {
|
||||
const CmdOutput = new CmdOutputBase();
|
||||
|
||||
/**
|
||||
* @typedef {import('./settings.mjs').KeyValueListeners } KeyValueListeners
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {KeyValueListeners}
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
@@ -112,7 +108,6 @@ function onFormSubmit(event) {
|
||||
|
||||
const cmd = event.target.elements
|
||||
.namedItem("cmd");
|
||||
|
||||
if (!(cmd instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createNodeList } from './relay.mjs';
|
||||
import { variableListeners } from './settings.mjs';
|
||||
|
||||
/** @returns {import('./settings.mjs').KeyValueListeners} */
|
||||
function listeners() {
|
||||
return {
|
||||
"dczRelays": (_, value) => {
|
||||
|
||||
@@ -56,8 +56,9 @@ export function showNotification(text) {
|
||||
* @param {number} lineno
|
||||
* @param {number} colno
|
||||
* @param {any} error
|
||||
* @return {string}
|
||||
*/
|
||||
function notify(message, source, lineno, colno, error) {
|
||||
export function formatErrorEvent(message, source, lineno, colno, error) {
|
||||
let text = "";
|
||||
if (message) {
|
||||
text += message;
|
||||
@@ -71,24 +72,28 @@ function notify(message, source, lineno, colno, error) {
|
||||
text += formatError(error);
|
||||
}
|
||||
|
||||
showNotification(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
/** @param {string} message */
|
||||
export function notifyMessage(message) {
|
||||
notify(message, "", 0, 0, null);
|
||||
showNotification(
|
||||
formatErrorEvent(message, "", 0, 0, null));
|
||||
}
|
||||
|
||||
/** @param {Error} error */
|
||||
export function notifyError(error) {
|
||||
notify("", "", 0, 0, error);
|
||||
showNotification(
|
||||
formatErrorEvent("", "", 0, 0, error));
|
||||
}
|
||||
|
||||
/** @param {ErrorEvent} event */
|
||||
export function notifyErrorEvent(event) {
|
||||
notify(event.message,
|
||||
event.filename,
|
||||
event.lineno,
|
||||
event.colno,
|
||||
event.error);
|
||||
showNotification(
|
||||
formatErrorEvent(
|
||||
event.message,
|
||||
event.filename,
|
||||
event.lineno,
|
||||
event.colno,
|
||||
event.error));
|
||||
}
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
import { sendAction } from './connection.mjs';
|
||||
import { variableListeners } from './settings.mjs';
|
||||
|
||||
function listeners() {
|
||||
return {
|
||||
"garlandBrightness": (_, value) => {
|
||||
const brightnessSlider = document.getElementById("garlandBrightness");
|
||||
brightnessSlider.value = value;
|
||||
},
|
||||
"garlandSpeed": (_, value) => {
|
||||
const speedSlider = document.getElementById("garlandSpeed");
|
||||
speedSlider.value = value;
|
||||
},
|
||||
};
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {function(HTMLInputElement): void} callback
|
||||
*/
|
||||
function withInputChange(selector, callback) {
|
||||
const elem = document.querySelector(selector);
|
||||
if (!(elem instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
elem.addEventListener("change", () => {
|
||||
callback(elem);
|
||||
});
|
||||
}
|
||||
|
||||
export function init() {
|
||||
variableListeners(listeners());
|
||||
|
||||
document.querySelector(".checkbox-garland-enable")
|
||||
.addEventListener("change", (event) => {
|
||||
sendAction("garland_switch", {status: event.target.checked ? 1 : 0});
|
||||
withInputChange(".checkbox-garland-enable",
|
||||
(elem) => {
|
||||
sendAction("garland_switch", {status: elem.checked ? 1 : 0});
|
||||
});
|
||||
|
||||
document.querySelector(".slider-garland-brightness")
|
||||
.addEventListener("change", (event) => {
|
||||
sendAction("garland_set_brightness", {brightness: event.target.value});
|
||||
withInputChange(".slider-garland-brightness",
|
||||
(elem) => {
|
||||
sendAction("garland_set_brightness", {brightness: elem.value});
|
||||
});
|
||||
|
||||
document.querySelector(".slider-garland-speed")
|
||||
.addEventListener("change", (event) => {
|
||||
sendAction("garland_set_speed", {speed: event.target.value});
|
||||
withInputChange(".slider-garland-speed",
|
||||
(elem) => {
|
||||
sendAction("garland_set_speed", {speed: elem.value});
|
||||
});
|
||||
|
||||
document.querySelector(".button-garland-set-default")
|
||||
.addEventListener("click", () => {
|
||||
?.addEventListener("click", () => {
|
||||
sendAction("garland_set_default", {});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import { addEnumerables, variableListeners } from './settings.mjs';
|
||||
import { showErrorNotification } from './errors.mjs';
|
||||
import { notifyMessage } from './errors.mjs';
|
||||
|
||||
function makeConfig(value) {
|
||||
let types = [];
|
||||
/**
|
||||
* @param {any} config
|
||||
*/
|
||||
function updateEnumerables(config) {
|
||||
/** @type {import('./settings.mjs').EnumerableEntry[]} */
|
||||
const types = [];
|
||||
|
||||
for (const [type, id] of value.types) {
|
||||
for (const [type, id] of /** @type {[string, number][]} */(config.types)) {
|
||||
types.push({
|
||||
"id": id,
|
||||
"name": type
|
||||
});
|
||||
|
||||
let gpios = [{"id": 153, "name": "NONE"}];
|
||||
value[type].forEach((pin) => {
|
||||
/** @type {import('./settings.mjs').EnumerableEntry[]} */
|
||||
const gpios = [{"id": 153, "name": "NONE"}];
|
||||
|
||||
/** @type {number[]} */
|
||||
(config[type]).forEach((pin) => {
|
||||
gpios.push({"id": pin, "name": `GPIO${pin}`});
|
||||
});
|
||||
|
||||
@@ -21,24 +28,32 @@ function makeConfig(value) {
|
||||
addEnumerables("gpio-types", types);
|
||||
}
|
||||
|
||||
function reportFailed(value) {
|
||||
let failed = "";
|
||||
for (const [pin, file, func, line] of value["failed-locks"]) {
|
||||
failed += `GPIO${pin} @ ${file}:${func}:${line}\n`;
|
||||
/** @param {[pin: number, file: string, func: string, line: number]} failed */
|
||||
function reportFailed(failed) {
|
||||
if (!failed.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
showErrorNotification("Could not acquire locks on the following pins, check configuration\n\n" + failed);
|
||||
let report = [];
|
||||
for (const [pin, file, func, line] of failed) {
|
||||
report.push(`GPIO${pin} @ ${file}:${func}:${line}`);
|
||||
}
|
||||
|
||||
notifyMessage(`
|
||||
Could not acquire locks on the following pins, check configuration
|
||||
\n\n${report.join("\n")}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"gpioConfig": (_, value) => {
|
||||
makeConfig(value);
|
||||
updateEnumerables(value);
|
||||
},
|
||||
"gpioInfo": (_, value) => {
|
||||
reportFailed(value);
|
||||
reportFailed(value["failed-locks"]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { sendAction } from './connection.mjs';
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
* @param {number} state
|
||||
*/
|
||||
function publishState(event, state) {
|
||||
event.preventDefault();
|
||||
sendAction("ha-publish", {state});
|
||||
}
|
||||
|
||||
/** @param {Event} event */
|
||||
function publishEnabled(event) {
|
||||
publishState(event, 1);
|
||||
}
|
||||
|
||||
/** @param {Event} event */
|
||||
function publishDisabled(event) {
|
||||
publishState(event, 0);
|
||||
}
|
||||
|
||||
export function init() {
|
||||
document.querySelector(".button-ha-enabled")
|
||||
.addEventListener("click", publishEnabled);
|
||||
?.addEventListener("click", publishEnabled);
|
||||
document.querySelector(".button-ha-disabled")
|
||||
.addEventListener("click", publishDisabled);
|
||||
?.addEventListener("click", publishDisabled);
|
||||
}
|
||||
|
||||
@@ -262,12 +262,12 @@ function askAndCallSimpleAction(event) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./settings.mjs').KeyValueListeners } KeyValueListeners
|
||||
*/
|
||||
// TODO https://github.com/microsoft/TypeScript/issues/58969
|
||||
// at-import'ed type becomes 'unused' for some reason
|
||||
// until fixed, prefer direct import vs. typedef
|
||||
|
||||
/**
|
||||
* @returns {KeyValueListeners}
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
@@ -329,7 +329,7 @@ function initSetupPassword(form) {
|
||||
event.preventDefault();
|
||||
const forms = [form];
|
||||
if (validateFormsPasswords(forms, true)) {
|
||||
applySettings(getData(forms, {cleanup: true}));
|
||||
applySettings(getData(forms, {cleanup: false}));
|
||||
}
|
||||
});
|
||||
document.querySelector(".button-generate-password")
|
||||
|
||||
@@ -1,37 +1,53 @@
|
||||
import {
|
||||
addSimpleEnumerables,
|
||||
fromSchema,
|
||||
groupSettingsOnAdd,
|
||||
groupSettingsOnAddElem,
|
||||
variableListeners,
|
||||
} from './settings.mjs';
|
||||
|
||||
import { addFromTemplate } from './template.mjs';
|
||||
import { addFromTemplate, addFromTemplateWithSchema } from './template.mjs';
|
||||
|
||||
function addNode(cfg) {
|
||||
addFromTemplate(document.getElementById("leds"), "led-config", cfg);
|
||||
/** @param {function(HTMLElement): void} callback */
|
||||
function withLeds(callback) {
|
||||
callback(/** @type {!HTMLElement} */
|
||||
(document.getElementById("leds")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elem
|
||||
*/
|
||||
function addLed(elem) {
|
||||
addFromTemplate(elem, "led-config", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
*/
|
||||
function onConfig(value) {
|
||||
withLeds((elem) => {
|
||||
addFromTemplateWithSchema(
|
||||
elem, "led-config",
|
||||
value.leds, value.schema,
|
||||
value.max ?? 0);
|
||||
});
|
||||
addSimpleEnumerables("led", "LED", value.leds.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"ledConfig": (_, value) => {
|
||||
let container = document.getElementById("leds");
|
||||
if (container.childElementCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
value.leds.forEach((entries) => {
|
||||
addNode(fromSchema(entries, value.schema));
|
||||
});
|
||||
|
||||
addSimpleEnumerables("led", "LED", value.leds.length);
|
||||
onConfig(value);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function init() {
|
||||
variableListeners(listeners());
|
||||
|
||||
groupSettingsOnAdd("leds", () => {
|
||||
addNode();
|
||||
withLeds((elem) => {
|
||||
variableListeners(listeners());
|
||||
groupSettingsOnAddElem(elem, () => {
|
||||
addLed(elem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,24 +4,46 @@ import { sendAction } from './connection.mjs';
|
||||
import { mergeTemplate, loadTemplate } from './template.mjs';
|
||||
import { addEnumerables, variableListeners } from './settings.mjs';
|
||||
|
||||
let ColorPicker = null;
|
||||
|
||||
function colorToHsvString(color) {
|
||||
var h = String(Math.round(color.hsv.h));
|
||||
var s = String(Math.round(color.hsv.s));
|
||||
var v = String(Math.round(color.hsv.v));
|
||||
return h + "," + s + "," + v;
|
||||
/** @param {function(HTMLElement): void} callback */
|
||||
function withPicker(callback) {
|
||||
const elem = document.getElementById("light-picker");
|
||||
if (elem) {
|
||||
callback(elem);
|
||||
}
|
||||
}
|
||||
|
||||
function hsvStringToColor(hsv) {
|
||||
var parts = hsv.split(",");
|
||||
/**
|
||||
* @param {iro.Color} color
|
||||
* @returns {string}
|
||||
*/
|
||||
function colorToHsvString(color) {
|
||||
const hsv =
|
||||
[color.hsv.h, color.hsv.s, color.hsv.v]
|
||||
.filter((value) => value !== undefined)
|
||||
.map(Math.round);
|
||||
|
||||
if (hsv.length !== 3) {
|
||||
return "0,0,0";
|
||||
}
|
||||
|
||||
return hsv.join(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} string
|
||||
*/
|
||||
function hsvStringToColor(string) {
|
||||
const parts = string.split(",")
|
||||
.map(parseInt);
|
||||
|
||||
return {
|
||||
h: parseInt(parts[0]),
|
||||
s: parseInt(parts[1]),
|
||||
v: parseInt(parts[2])
|
||||
h: parts[0] ?? 0,
|
||||
s: parts[1] ?? 0,
|
||||
v: parts[2] ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {string} type */
|
||||
function colorSlider(type) {
|
||||
return {component: iro.ui.Slider, options: {sliderType: type}};
|
||||
}
|
||||
@@ -34,36 +56,50 @@ function colorBox() {
|
||||
return {component: iro.ui.Box, options: {}};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {"rgb" | "hsv"} mode
|
||||
* @param {string} value
|
||||
*/
|
||||
function colorUpdate(mode, value) {
|
||||
if ("rgb" === mode) {
|
||||
ColorPicker.color.hexString = value;
|
||||
} else if ("hsv" === mode) {
|
||||
ColorPicker.color.hsv = hsvStringToColor(value);
|
||||
}
|
||||
withPicker((elem) => {
|
||||
elem.dispatchEvent(
|
||||
new CustomEvent("color:string", {
|
||||
detail: {
|
||||
mode: mode,
|
||||
value: value,
|
||||
}}));
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {number} id */
|
||||
function lightStateHideRelay(id) {
|
||||
styleInject([
|
||||
styleVisible(`.relay-control-${id}`, false)
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param {function(HTMLInputElement): void} callback */
|
||||
function withState(callback) {
|
||||
const elem = document.getElementById("light-state-value");
|
||||
if (elem instanceof HTMLInputElement) {
|
||||
callback(elem);
|
||||
}
|
||||
}
|
||||
|
||||
function initLightState() {
|
||||
const toggle = document.getElementById("light-state-value");
|
||||
toggle.addEventListener("change", (event) => {
|
||||
event.preventDefault();
|
||||
sendAction("light", {state: event.target.checked});
|
||||
withState((elem) => {
|
||||
elem.addEventListener("change", (event) => {
|
||||
event.preventDefault();
|
||||
sendAction("light", {state: /** @type {!HTMLInputElement} */(event.target).checked});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function updateLightState(value) {
|
||||
const state = document.getElementById("light-state-value");
|
||||
state.checked = value;
|
||||
colorPickerState(value);
|
||||
}
|
||||
|
||||
/** @param {boolean} value */
|
||||
function colorPickerState(value) {
|
||||
const light = document.getElementById("light");
|
||||
const light = /** @type {!HTMLElement} */
|
||||
(document.getElementById("light"));
|
||||
if (value) {
|
||||
light.classList.add("light-on");
|
||||
} else {
|
||||
@@ -71,20 +107,35 @@ function colorPickerState(value) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {boolean} value */
|
||||
function updateLightState(value) {
|
||||
withState((elem) => {
|
||||
elem.checked = value;
|
||||
colorPickerState(value);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {boolean} value */
|
||||
function colorEnabled(value) {
|
||||
if (value) {
|
||||
lightAddClass("light-color");
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {boolean} value */
|
||||
function colorInit(value) {
|
||||
// TODO: ref. #2451, input:change causes pretty fast updates.
|
||||
// either make sure we don't cause any issue on the esp, or switch to
|
||||
// color:change instead (which applies after input ends)
|
||||
|
||||
/** @type {function(iro.Color): void} */
|
||||
let change = () => {
|
||||
};
|
||||
|
||||
/** @type {string[]} */
|
||||
const rules = [];
|
||||
|
||||
/** @type {{"component": any, "options": any}[]} */
|
||||
const layout = [];
|
||||
|
||||
// RGB
|
||||
@@ -110,57 +161,115 @@ function colorInit(value) {
|
||||
layout.push(colorSlider("value"));
|
||||
styleInject(rules);
|
||||
|
||||
ColorPicker = new iro.ColorPicker("#light-picker", {layout});
|
||||
ColorPicker.on("input:change", change);
|
||||
withPicker((elem) => {
|
||||
// TODO tsserver does not like the resulting type
|
||||
const picker = new /** @type {any} */
|
||||
(iro.ColorPicker(elem, {layout}));
|
||||
picker.on("input:change", change);
|
||||
|
||||
elem.addEventListener("color:string", (event) => {
|
||||
const color = /** @type {CustomEvent<{mode: string, value: string}>} */
|
||||
(event).detail;
|
||||
switch (color.mode) {
|
||||
case "rgb":
|
||||
picker.color.hexString = color.value;
|
||||
break;
|
||||
case "hsv":
|
||||
picker.color.hsv = hsvStringToColor(color.value);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateMireds(value) {
|
||||
const mireds = document.getElementById("mireds-value");
|
||||
if (mireds !== null) {
|
||||
mireds.value = value;
|
||||
mireds.nextElementSibling.textContent = value;
|
||||
/** @param {function(HTMLInputElement): void} callback */
|
||||
function withMiredsValue(callback) {
|
||||
const elem = document.getElementById("mireds-value");
|
||||
if (elem instanceof HTMLInputElement) {
|
||||
callback(elem);
|
||||
}
|
||||
}
|
||||
|
||||
function lightAddClass(className) {
|
||||
const light = document.getElementById("light");
|
||||
light.classList.add(className);
|
||||
/**
|
||||
* @param {HTMLElement} elem
|
||||
* @param {string} text
|
||||
*/
|
||||
function textForNextSibling(elem, text) {
|
||||
const next = elem.nextElementSibling;
|
||||
if (!next) {
|
||||
return;
|
||||
}
|
||||
|
||||
next.textContent = text;
|
||||
}
|
||||
|
||||
// White implies we should hide one or both white channels
|
||||
/**
|
||||
* @param {HTMLInputElement} elem
|
||||
* @param {number} value
|
||||
*/
|
||||
function setMiredsValue(elem, value) {
|
||||
elem.value = value.toString();
|
||||
textForNextSibling(elem, elem.value);
|
||||
}
|
||||
|
||||
/** @param {number} value */
|
||||
function updateMireds(value) {
|
||||
withMiredsValue((elem) => {
|
||||
setMiredsValue(elem, value);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {string} className */
|
||||
function lightAddClass(className) {
|
||||
const light = document.getElementById("light");
|
||||
light?.classList?.add(className);
|
||||
}
|
||||
|
||||
/**
|
||||
* implies we should hide one or both white channels
|
||||
* @param {boolean} value
|
||||
*/
|
||||
function whiteEnabled(value) {
|
||||
if (value) {
|
||||
lightAddClass("light-white");
|
||||
}
|
||||
}
|
||||
|
||||
// When there are CCT controls, no need for raw white channel sliders
|
||||
/**
|
||||
* no need for raw white channel sliders with cct
|
||||
* @param {boolean} value
|
||||
*/
|
||||
function cctEnabled(value) {
|
||||
if (value) {
|
||||
lightAddClass("light-cct");
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {{cold: number, warm: number}} value */
|
||||
function cctInit(value) {
|
||||
const control = loadTemplate("mireds-control");
|
||||
|
||||
const slider = control.getElementById("mireds-value");
|
||||
slider.setAttribute("min", value.cold);
|
||||
slider.setAttribute("max", value.warm);
|
||||
slider.addEventListener("change", (event) => {
|
||||
event.target.nextElementSibling.textContent = event.target.value;
|
||||
sendAction("light", {mireds: event.target.value});
|
||||
withMiredsValue((elem) => {
|
||||
elem.setAttribute("min", value.cold.toString());
|
||||
elem.setAttribute("max", value.warm.toString());
|
||||
elem.addEventListener("change", () => {
|
||||
textForNextSibling(elem, elem.value);
|
||||
sendAction("light", {mireds: elem.value});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
const datalist = control.querySelector("datalist");
|
||||
const [datalist] = control.querySelectorAll("datalist");
|
||||
datalist.innerHTML = `
|
||||
<option value="${value.cold}">Cold</option>
|
||||
<option value="${value.warm}">Warm</option>
|
||||
`;
|
||||
<option value="${value.cold}">Cold</option>
|
||||
<option value="${value.warm}">Warm</option>`;
|
||||
|
||||
mergeTemplate(document.getElementById("light-cct"), control);
|
||||
mergeTemplate(
|
||||
/** @type {HTMLElement} */
|
||||
(document.getElementById("light-cct")), control);
|
||||
}
|
||||
|
||||
/** @param {{[k: string]: any}} data */
|
||||
function updateLight(data) {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
switch (key) {
|
||||
@@ -202,60 +311,95 @@ function updateLight(data) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {Event} event */
|
||||
function onChannelSliderChange(event) {
|
||||
event.target.nextElementSibling.textContent = event.target.value;
|
||||
if (!(event.target instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let channel = {}
|
||||
channel[event.target.dataset["id"]] = event.target.value;
|
||||
const target = event.target;
|
||||
textForNextSibling(target, target.value);
|
||||
|
||||
const id = target.dataset["id"];
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendAction("light", {channel});
|
||||
sendAction("light", {
|
||||
channel: {
|
||||
[id]: target.value
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {Event} event */
|
||||
function onBrightnessSliderChange(event) {
|
||||
event.target.nextElementSibling.textContent = event.target.value;
|
||||
if (!(event.target instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
textForNextSibling(event.target, event.target.value);
|
||||
sendAction("light", {brightness: event.target.value});
|
||||
}
|
||||
|
||||
function initBrightness() {
|
||||
const template = loadTemplate("brightness-control");
|
||||
|
||||
const slider = template.getElementById("brightness-value");
|
||||
slider.addEventListener("change", onBrightnessSliderChange);
|
||||
const elem = template.getElementById("brightness-value");
|
||||
elem?.addEventListener("change", onBrightnessSliderChange);
|
||||
|
||||
mergeTemplate(document.getElementById("light-brightness"), template);
|
||||
mergeTemplate(
|
||||
/** @type {!HTMLElement} */
|
||||
(document.getElementById("light-brightness")), template);
|
||||
}
|
||||
|
||||
/** @param {number} value */
|
||||
function updateBrightness(value) {
|
||||
const brightness = document.getElementById("brightness-value");
|
||||
if (brightness !== null) {
|
||||
brightness.value = value;
|
||||
brightness.nextElementSibling.textContent = value;
|
||||
const elem = document.getElementById("brightness-value");
|
||||
if (elem instanceof HTMLInputElement) {
|
||||
elem.value = value.toString();
|
||||
textForNextSibling(elem, elem.value);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string[]} channels */
|
||||
function initChannels(channels) {
|
||||
const container = document.getElementById("light-channels");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {import('./settings.mjs').EnumerableEntry[]} */
|
||||
const enumerables = [];
|
||||
|
||||
channels.forEach((tag, channel) => {
|
||||
const line = loadTemplate("channel-control");
|
||||
line.querySelector("span.slider").dataset["id"] = channel;
|
||||
line.querySelector("div").setAttribute("id", `light-channel-${tag.toLowerCase()}`);
|
||||
|
||||
const slider = line.querySelector("input.slider");
|
||||
slider.dataset["id"] = channel;
|
||||
const [root] = line.querySelectorAll("div");
|
||||
root.setAttribute("id", `light-channel-${tag.toLowerCase()}`);
|
||||
|
||||
const name =
|
||||
`Channel #${channel} (${tag.toUpperCase()})`;
|
||||
|
||||
const [label] = line.querySelectorAll("label");
|
||||
label.textContent = name;
|
||||
|
||||
enumerables.push({"id": channel, "name": name});
|
||||
|
||||
const [span] = line.querySelectorAll("span");
|
||||
span.dataset["id"] = channel.toString();
|
||||
|
||||
const [slider] = line.querySelectorAll("input");
|
||||
slider.dataset["id"] = channel.toString();
|
||||
slider.addEventListener("change", onChannelSliderChange);
|
||||
|
||||
const label = `Channel #${channel} (${tag.toUpperCase()})`;
|
||||
line.querySelector("label").textContent = label;
|
||||
mergeTemplate(container, line);
|
||||
|
||||
enumerables.push({"id": channel, "name": label});
|
||||
});
|
||||
|
||||
addEnumerables("Channels", enumerables);
|
||||
}
|
||||
|
||||
/** @param {number[]} values */
|
||||
function updateChannels(values) {
|
||||
const container = document.getElementById("light");
|
||||
if (!container) {
|
||||
@@ -264,15 +408,18 @@ function updateChannels(values) {
|
||||
|
||||
values.forEach((value, channel) => {
|
||||
const slider = container.querySelector(`input.slider[data-id='${channel}']`);
|
||||
if (!slider) {
|
||||
if (!(slider instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
slider.value = value;
|
||||
slider.nextElementSibling.textContent = value;
|
||||
slider.value = value.toString();
|
||||
textForNextSibling(slider, slider.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"light": (_, value) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ function clear() {
|
||||
|
||||
export function init() {
|
||||
document.querySelector(".button-lightfox-learn")
|
||||
.addEventListener("click", learn);
|
||||
?.addEventListener("click", learn);
|
||||
document.querySelector(".button-lightfox-clear")
|
||||
.addEventListener("click", clear);
|
||||
?.addEventListener("click", clear);
|
||||
}
|
||||
|
||||
@@ -174,11 +174,7 @@ async function onFileChanged(event) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./settings.mjs').KeyValueListeners } KeyValueListeners
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {KeyValueListeners}
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label>Enabled</label>
|
||||
<input class="checkbox-toggle checkbox-garland-enable" type="checkbox" name="garlandEnabled">
|
||||
<input class="checkbox-toggle checkbox-garland-enable" type="checkbox" name="garlandEnabled" data-action="none">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label>Brightness</label>
|
||||
<input class="slider slider-garland-brightness pure-input-3-4" type="range" min="12" max="254" step="22" id="garlandBrightness">
|
||||
<input class="slider slider-garland-brightness pure-input-3-4" type="range" min="12" max="254" step="22" id="garlandBrightness" data-action="none">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label>Speed</label>
|
||||
<input class="slider slider-garland-speed pure-input-3-4" type="range" min="10" max="70" id="garlandSpeed">
|
||||
<input class="slider slider-garland-speed pure-input-3-4" type="range" min="10" max="70" id="garlandSpeed" data-action="none">
|
||||
</div>
|
||||
|
||||
<button type="button" class="pure-button button-garland-set-default">Set default</button>
|
||||
|
||||
@@ -1,125 +1,177 @@
|
||||
import {
|
||||
addEnumerables,
|
||||
fromSchema,
|
||||
getEnumerables,
|
||||
variableListeners,
|
||||
} from './settings.mjs';
|
||||
import { addFromTemplate } from './template.mjs';
|
||||
|
||||
import { sendAction } from './connection.mjs';
|
||||
|
||||
import {
|
||||
addFromTemplate,
|
||||
fromSchema,
|
||||
loadTemplate,
|
||||
loadConfigTemplate,
|
||||
mergeTemplate,
|
||||
NumberInput,
|
||||
} from './template.mjs';
|
||||
|
||||
import {
|
||||
addEnumerables,
|
||||
getEnumerables,
|
||||
setOriginalsFromValues,
|
||||
variableListeners,
|
||||
} from './settings.mjs';
|
||||
|
||||
/** @param {Event} event */
|
||||
function onToggle(event) {
|
||||
event.preventDefault();
|
||||
sendAction("relay", {
|
||||
id: parseInt(event.target.dataset["id"], 10),
|
||||
status: event.target.checked ? "1" : "0"});
|
||||
}
|
||||
|
||||
function initToggle(id, cfg) {
|
||||
const line = loadTemplate("relay-control");
|
||||
|
||||
let root = line.querySelector("div");
|
||||
root.classList.add(`relay-control-${id}`);
|
||||
|
||||
let name = line.querySelector("span[data-key='relayName']");
|
||||
name.textContent = cfg.relayName;
|
||||
name.dataset["id"] = id;
|
||||
name.setAttribute("title", cfg.relayProv);
|
||||
|
||||
let realId = "relay".concat(id);
|
||||
|
||||
let toggle = line.querySelector("input[type='checkbox']");
|
||||
toggle.checked = false;
|
||||
toggle.disabled = true;
|
||||
toggle.dataset["id"] = id;
|
||||
toggle.addEventListener("change", onToggle);
|
||||
|
||||
toggle.setAttribute("id", realId);
|
||||
toggle.previousElementSibling.setAttribute("for", realId);
|
||||
|
||||
mergeTemplate(document.getElementById("relays"), line);
|
||||
}
|
||||
|
||||
function updateState(data) {
|
||||
data.states.forEach((state, id) => {
|
||||
const relay = fromSchema(state, data.schema);
|
||||
|
||||
let elem = document.querySelector(`input[name='relay'][data-id='${id}']`);
|
||||
elem.checked = relay.status;
|
||||
elem.disabled = ({
|
||||
0: false,
|
||||
1: !relay.status,
|
||||
2: relay.status
|
||||
})[relay.lock]; // TODO: specify lock statuses earlier?
|
||||
});
|
||||
}
|
||||
|
||||
function addConfigNode(cfg) {
|
||||
addFromTemplate(document.getElementById("relayConfig"), "relay-config", cfg);
|
||||
}
|
||||
|
||||
function listeners() {
|
||||
return {
|
||||
"relayConfig": (_, value) => {
|
||||
let container = document.getElementById("relays");
|
||||
if (container.childElementCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let relays = [];
|
||||
value.relays.forEach((entries, id) => {
|
||||
let cfg = fromSchema(entries, value.schema);
|
||||
if (!cfg.relayName || !cfg.relayName.length) {
|
||||
cfg.relayName = `Switch #${id}`;
|
||||
}
|
||||
|
||||
relays.push({
|
||||
"id": id,
|
||||
"name": `${cfg.relayName} (${cfg.relayProv})`
|
||||
});
|
||||
|
||||
initToggle(id, cfg);
|
||||
addConfigNode(cfg);
|
||||
});
|
||||
|
||||
addEnumerables("relay", relays);
|
||||
},
|
||||
"relayState": (_, value) => {
|
||||
updateState(value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createNodeList(containerId, values, keyPrefix) {
|
||||
const target = document.getElementById(containerId);
|
||||
if (target.childElementCount > 0) {
|
||||
const target = /** @type {!HTMLInputElement} */(event.target);
|
||||
const id = target.dataset["id"];
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: let schema set the settings key
|
||||
const fragment = loadConfigTemplate("number-input");
|
||||
values.forEach((value, index) => {
|
||||
const line = fragment.cloneNode(true);
|
||||
sendAction("relay", {
|
||||
id: parseInt(id, 10),
|
||||
status: target.checked ? "1" : "0"});
|
||||
}
|
||||
|
||||
const enumerables = getEnumerables("relay");
|
||||
line.querySelector("label").textContent = (enumerables)
|
||||
? enumerables[index].name : `Switch #${index}`;
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {any} cfg
|
||||
*/
|
||||
function initToggle(id, cfg) {
|
||||
const container = document.getElementById("relays");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = line.querySelector("input");
|
||||
input.name = keyPrefix;
|
||||
input.value = value;
|
||||
input.dataset["original"] = value;
|
||||
const line = loadTemplate("relay-control");
|
||||
|
||||
mergeTemplate(target, line);
|
||||
const root = /** @type {!HTMLDivElement} */
|
||||
(line.querySelector("div"));
|
||||
root.classList.add(`relay-control-${id}`);
|
||||
|
||||
const name = /** @type {!HTMLSpanElement} */
|
||||
(line.querySelector("span[data-key='relayName']"));
|
||||
name.textContent = cfg.relayName;
|
||||
name.dataset["id"] = id.toString();
|
||||
name.setAttribute("title", cfg.relayProv);
|
||||
|
||||
const toggle = /** @type {!HTMLInputElement} */
|
||||
(line.querySelector("input[type='checkbox']"));
|
||||
toggle.checked = false;
|
||||
toggle.disabled = true;
|
||||
toggle.dataset["id"] = id.toString();
|
||||
toggle.addEventListener("change", onToggle);
|
||||
|
||||
const realId = `relay${id}`;
|
||||
toggle.setAttribute("id", realId);
|
||||
toggle.previousElementSibling?.setAttribute("for", realId);
|
||||
|
||||
mergeTemplate(container, line);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any[]} states
|
||||
* @param {string[]} schema
|
||||
*/
|
||||
function updateFromState(states, schema) {
|
||||
states.forEach((state, id) => {
|
||||
const elem = /** @type {!HTMLInputElement} */
|
||||
(document.querySelector(`input[name='relay'][data-id='${id}']`));
|
||||
|
||||
const relay = fromSchema(state, schema);
|
||||
|
||||
const status = /** @type {boolean} */(relay.status);
|
||||
elem.checked = status;
|
||||
|
||||
// TODO: publish possible enum values in ws init
|
||||
const as_lock = new Map();
|
||||
as_lock.set(0, false);
|
||||
as_lock.set(1, !status);
|
||||
as_lock.set(2, status);
|
||||
|
||||
const lock = /** @type {number} */(relay.lock);
|
||||
if (as_lock.has(lock)) {
|
||||
elem.disabled = as_lock.get(lock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {any} cfg */
|
||||
function addConfigNode(cfg) {
|
||||
const container = /** @type {!HTMLElement} */
|
||||
(document.getElementById("relayConfig"));
|
||||
addFromTemplate(container, "relay-config", cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any[]} configs
|
||||
* @param {string[]} schema
|
||||
*/
|
||||
function updateFromConfig(configs, schema) {
|
||||
const container = document.getElementById("relays");
|
||||
if (!container || container.childElementCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {import('./settings.mjs').EnumerableEntry[]} */
|
||||
const relays = [];
|
||||
|
||||
configs.forEach((config, id) => {
|
||||
const relay = fromSchema(config, schema);
|
||||
if (!relay.relayName) {
|
||||
relay.relayName = `Switch #${id}`;
|
||||
}
|
||||
|
||||
relays.push({
|
||||
"id": id,
|
||||
"name": `${relay.relayName} (${relay.relayProv})`
|
||||
});
|
||||
|
||||
initToggle(id, relay);
|
||||
addConfigNode(relay);
|
||||
});
|
||||
|
||||
addEnumerables("relay", relays);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {any[]} values
|
||||
* @param {string} keyPrefix
|
||||
*/
|
||||
export function createNodeList(id, values, keyPrefix) {
|
||||
const container = document.getElementById(id);
|
||||
if (!container || container.childElementCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO generic template to automatically match elems to (limited) schema?
|
||||
const template = new NumberInput();
|
||||
values.forEach((value, index) => {
|
||||
mergeTemplate(container, template.with(
|
||||
(label, input) => {
|
||||
const enumerables = getEnumerables("relay");
|
||||
label.textContent =
|
||||
(enumerables)
|
||||
? enumerables[index].name
|
||||
: `Switch #${index}`;
|
||||
|
||||
input.name = keyPrefix;
|
||||
input.value = value;
|
||||
setOriginalsFromValues([input]);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/** @returns {import('./settings.mjs').KeyValueListeners} */
|
||||
function listeners() {
|
||||
return {
|
||||
"relayConfig": (_, value) => {
|
||||
updateFromConfig(value.relays, value.schema);
|
||||
},
|
||||
"relayState": (_, value) => {
|
||||
updateFromState(value.relays, value.schema);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function init() {
|
||||
variableListeners(listeners());
|
||||
}
|
||||
|
||||
@@ -11,33 +11,51 @@ import {
|
||||
loadConfigTemplate,
|
||||
} from './template.mjs';
|
||||
|
||||
/** @param {Event} event */
|
||||
function onButtonPress(event) {
|
||||
if (!(event.target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = "button-rfb-";
|
||||
const [buttonRfbClass] = Array.from(event.target.classList)
|
||||
.filter(x => x.startsWith(prefix));
|
||||
|
||||
if (buttonRfbClass) {
|
||||
const container = event.target.parentElement.parentElement;
|
||||
const input = container.querySelector("input");
|
||||
|
||||
sendAction(`rfb${buttonRfbClass.replace(prefix, "")}`, {
|
||||
id: parseInt(input.dataset["id"], 10),
|
||||
status: input.name === "rfbON"
|
||||
});
|
||||
if (!buttonRfbClass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = event.target?.parentElement?.parentElement;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = container.querySelector("input");
|
||||
if (!input || !input.dataset["id"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendAction(`rfb${buttonRfbClass.replace(prefix, "")}`, {
|
||||
id: parseInt(input.dataset["id"], 10),
|
||||
status: input.name === "rfbON"
|
||||
});
|
||||
}
|
||||
|
||||
function addNode() {
|
||||
let container = document.getElementById("rfbNodes");
|
||||
const container = document.getElementById("rfbNodes");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = container.childElementCount;
|
||||
const id = container.childElementCount.toString();
|
||||
const line = loadConfigTemplate("rfb-node");
|
||||
line.querySelector("span").textContent = id;
|
||||
|
||||
for (let input of line.querySelectorAll("input")) {
|
||||
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}`)) {
|
||||
@@ -48,21 +66,31 @@ function addNode() {
|
||||
mergeTemplate(container, line);
|
||||
}
|
||||
|
||||
function handleCodes(value) {
|
||||
value.codes.forEach((codes, id) => {
|
||||
const realId = id + value.start;
|
||||
const [off, on] = codes;
|
||||
/**
|
||||
* @typedef {[string, string]} CodePair
|
||||
* @param {CodePair[]} pairs
|
||||
* @param {!number} start
|
||||
*/
|
||||
function handleCodePairs(pairs, start) {
|
||||
pairs.forEach((pair, id) => {
|
||||
const realId = id + (start ?? 0);
|
||||
const [off, on] = pair;
|
||||
|
||||
const rfbOn = document.getElementById(`rfbON${realId}`);
|
||||
setInputValue(rfbOn, on);
|
||||
|
||||
const rfbOff = document.getElementById(`rfbOFF${realId}`);
|
||||
const rfbOff = /** @type {!HTMLInputElement} */
|
||||
(document.getElementById(`rfbOFF${realId}`));
|
||||
setInputValue(rfbOff, off);
|
||||
|
||||
const rfbOn = /** @type {!HTMLInputElement} */
|
||||
(document.getElementById(`rfbON${realId}`));
|
||||
setInputValue(rfbOn, on);
|
||||
|
||||
setOriginalsFromValues([rfbOn, rfbOff]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"rfbCount": (_, value) => {
|
||||
@@ -71,7 +99,7 @@ function listeners() {
|
||||
}
|
||||
},
|
||||
"rfb": (_, value) => {
|
||||
handleCodes(value);
|
||||
handleCodePairs(value.codes, value.start);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,34 +1,55 @@
|
||||
import { addFromTemplate } from './template.mjs';
|
||||
import { groupSettingsOnAdd, fromSchema, variableListeners } from './settings.mjs';
|
||||
import { addFromTemplate, addFromTemplateWithSchema } from './template.mjs';
|
||||
import { groupSettingsOnAddElem, variableListeners } from './settings.mjs';
|
||||
import { sendAction } from './connection.mjs';
|
||||
|
||||
let State = {
|
||||
filters: {}
|
||||
};
|
||||
/**
|
||||
* @typedef {Map<number, string>} FiltersMap
|
||||
*/
|
||||
|
||||
function addMapping(cfg) {
|
||||
addFromTemplate(document.getElementById("rfm69-mapping"), "rfm69-node", cfg);
|
||||
}
|
||||
|
||||
function messages() {
|
||||
let [body] = document.getElementById("rfm69-messages").tBodies;
|
||||
return body;
|
||||
}
|
||||
|
||||
function rows() {
|
||||
return messages().rows;
|
||||
/** @type {FiltersMap} */
|
||||
const Filters = new Map();
|
||||
|
||||
/** @param {function(HTMLTableElement): void} callback */
|
||||
function withMessages(callback) {
|
||||
callback(/** @type {!HTMLTableElement} */
|
||||
(document.getElementById("rfm69-messages")));
|
||||
}
|
||||
|
||||
/** @param {[number, number, number, string, string, number, number, number]} message */
|
||||
function addMessage(message) {
|
||||
let timestamp = (new Date()).toLocaleTimeString("en-US", {hour12: false});
|
||||
withMessages((elem) => {
|
||||
const timestamp = (new Date())
|
||||
.toLocaleTimeString("en-US", {hour12: false});
|
||||
|
||||
let container = messages();
|
||||
let row = container.insertRow();
|
||||
for (let value of [timestamp, ...message]) {
|
||||
let cell = row.insertCell();
|
||||
cell.appendChild(document.createTextNode(value));
|
||||
filterRow(State.filters, row);
|
||||
}
|
||||
const row = elem.tBodies[0].insertRow();
|
||||
for (let value of [timestamp, ...message]) {
|
||||
const cell = row.insertCell();
|
||||
cell.appendChild(
|
||||
document.createTextNode(value.toString()));
|
||||
filterRow(Filters, row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {function(HTMLElement): void} callback */
|
||||
function withMapping(callback) {
|
||||
callback(/** @type {!HTMLElement} */
|
||||
(document.getElementById("rfm69-mapping")));
|
||||
}
|
||||
|
||||
/** @param {HTMLElement} elem */
|
||||
function addMappingNode(elem) {
|
||||
addFromTemplate(elem, "rfm69-node", {});
|
||||
}
|
||||
|
||||
/** @param {any} value */
|
||||
function onMapping(value) {
|
||||
withMapping((elem) => {
|
||||
addFromTemplateWithSchema(
|
||||
elem, "rfm69-node",
|
||||
value.mapping, value.schema,
|
||||
value.max ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
function clearCounters() {
|
||||
@@ -37,59 +58,84 @@ function clearCounters() {
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
let container = messages();
|
||||
while (container.rows.length) {
|
||||
container.deleteRow(0);
|
||||
}
|
||||
withMessages((elem) => {
|
||||
while (elem.rows.length) {
|
||||
elem.deleteRow(0);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FiltersMap} filters
|
||||
* @param {HTMLTableRowElement} row
|
||||
*/
|
||||
function filterRow(filters, row) {
|
||||
row.style.display = "table-row";
|
||||
for (const [cell, filter] of Object.entries(filters)) {
|
||||
for (const [cell, filter] of filters) {
|
||||
if (row.cells[cell].textContent !== filter) {
|
||||
row.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FiltersMap} filters
|
||||
* @param {HTMLTableRowElement[]} rows
|
||||
*/
|
||||
function filterRows(filters, rows) {
|
||||
for (let row of rows) {
|
||||
filterRow(filters, row);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {Event} event */
|
||||
function filterEvent(event) {
|
||||
if (event.target.classList.contains("filtered")) {
|
||||
delete State.filters[event.target.cellIndex];
|
||||
} else {
|
||||
State.filters[event.target.cellIndex] = event.target.textContent;
|
||||
if (!(event.target instanceof HTMLTableCellElement)) {
|
||||
return;
|
||||
}
|
||||
event.target.classList.toggle("filtered");
|
||||
|
||||
filterRows(State.filters, rows());
|
||||
if (!event.target.textContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = event.target.cellIndex;
|
||||
if (event.target.classList.contains("filtered")) {
|
||||
Filters.delete(index);
|
||||
} else {
|
||||
Filters.set(index, event.target.textContent);
|
||||
}
|
||||
|
||||
event.target.classList.toggle("filtered");
|
||||
withMessages((elem) => {
|
||||
filterRows(Filters, Array.from(elem.rows));
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
let container = messages();
|
||||
for (let elem of container.querySelectorAll("filtered")) {
|
||||
elem.classList.remove("filtered");
|
||||
}
|
||||
withMessages((elem) => {
|
||||
for (let filtered of elem.querySelectorAll("filtered")) {
|
||||
filtered.classList.remove("filtered");
|
||||
}
|
||||
|
||||
State.filters = {};
|
||||
filterRows(State.filters, container.rows);
|
||||
Filters.clear();
|
||||
filterRows(Filters, Array.from(elem.rows));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"rfm69": (_, value) => {
|
||||
if (value.message !== undefined) {
|
||||
addMessage(value.message);
|
||||
}
|
||||
|
||||
if (value.mapping !== undefined) {
|
||||
value.mapping.forEach((mapping) => {
|
||||
addMapping(fromSchema(mapping, value.schema));
|
||||
});
|
||||
onMapping(value);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -99,16 +145,18 @@ export function init() {
|
||||
variableListeners(listeners());
|
||||
|
||||
document.querySelector(".button-clear-counts")
|
||||
.addEventListener("click", clearCounters);
|
||||
?.addEventListener("click", clearCounters);
|
||||
document.querySelector(".button-clear-messages")
|
||||
.addEventListener("click", clearMessages);
|
||||
?.addEventListener("click", clearMessages);
|
||||
|
||||
document.querySelector(".button-clear-filters")
|
||||
.addEventListener("click", clearFilters);
|
||||
?.addEventListener("click", clearFilters);
|
||||
document.querySelector("#rfm69-messages tbody")
|
||||
.addEventListener("click", filterEvent);
|
||||
?.addEventListener("click", filterEvent);
|
||||
|
||||
groupSettingsOnAdd("rfm69-mapping", () => {
|
||||
addMapping();
|
||||
withMapping((elem) => {
|
||||
groupSettingsOnAddElem(elem, () => {
|
||||
addMappingNode(elem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,24 +1,57 @@
|
||||
import { groupSettingsOnAdd, variableListeners, fromSchema } from './settings.mjs';
|
||||
import { addFromTemplate } from './template.mjs';
|
||||
import { groupSettingsOnAddElem, variableListeners } from './settings.mjs';
|
||||
import { addFromTemplateWithSchema, addFromTemplate } from './template.mjs';
|
||||
|
||||
function addRule(cfg) {
|
||||
addFromTemplate(document.getElementById("rpn-rules"), "rpn-rule", cfg);
|
||||
/** @param {function(HTMLElement): void} callback */
|
||||
function withRules(callback) {
|
||||
callback(/** @type {!HTMLElement} */
|
||||
(document.getElementById("rpn-rules")));
|
||||
}
|
||||
|
||||
function addTopic(cfg) {
|
||||
addFromTemplate(document.getElementById("rpn-topics"), "rpn-topic", cfg);
|
||||
/**
|
||||
* @param {HTMLElement} elem
|
||||
* @param {string} rule
|
||||
*/
|
||||
function addRule(elem, rule = "") {
|
||||
addFromTemplate(elem, "rpn-rule", {rpnRule: rule});
|
||||
}
|
||||
|
||||
/** @param {function(HTMLElement): void} callback */
|
||||
function withTopics(callback) {
|
||||
callback(/** @type {!HTMLElement} */
|
||||
(document.getElementById("rpn-topics")));
|
||||
}
|
||||
|
||||
/** @param {HTMLElement} elem */
|
||||
function addTopic(elem) {
|
||||
addFromTemplate(elem, "rpn-topic", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elem
|
||||
* @param {any} value
|
||||
*/
|
||||
function addTopicWithSchema(elem, value) {
|
||||
addFromTemplateWithSchema(
|
||||
elem, "rpn-topic",
|
||||
value.topics, value.schema,
|
||||
value.max ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"rpnRules": (_, value) => {
|
||||
for (let rule of value) {
|
||||
addRule({"rpnRule": rule});
|
||||
}
|
||||
withRules((elem) => {
|
||||
for (let rule of value) {
|
||||
addRule(elem, rule);
|
||||
}
|
||||
});
|
||||
},
|
||||
"rpnTopics": (_, value) => {
|
||||
value.topics.forEach((topic) => {
|
||||
addTopic(fromSchema(topic, value.schema));
|
||||
withTopics((elem) => {
|
||||
addTopicWithSchema(elem, value);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -26,10 +59,14 @@ function listeners() {
|
||||
|
||||
export function init() {
|
||||
variableListeners(listeners());
|
||||
groupSettingsOnAdd("rpn-rules", () => {
|
||||
addRule();
|
||||
withRules((elem) => {
|
||||
groupSettingsOnAddElem(elem, () => {
|
||||
addRule(elem);
|
||||
});
|
||||
});
|
||||
groupSettingsOnAdd("rpn-topics", () => {
|
||||
addTopic();
|
||||
withTopics((elem) => {
|
||||
groupSettingsOnAddElem(elem, () => {
|
||||
addTopic(elem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
import { addFromTemplate } from './template.mjs';
|
||||
import { groupSettingsOnAdd, variableListeners, fromSchema } from './settings.mjs';
|
||||
import { addFromTemplate, addFromTemplateWithSchema } from './template.mjs';
|
||||
import { groupSettingsOnAddElem, variableListeners } from './settings.mjs';
|
||||
|
||||
function addNode(cfg) {
|
||||
addFromTemplate(document.getElementById("schedules"), "schedule-config", cfg);
|
||||
/** @param {function(HTMLElement): void} callback */
|
||||
function withSchedules(callback) {
|
||||
callback(/** @type {!HTMLElement} */
|
||||
(document.getElementById("schedules")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elem
|
||||
*/
|
||||
function scheduleAdd(elem) {
|
||||
addFromTemplate(elem, "schedule-config", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
*/
|
||||
function onConfig(value) {
|
||||
withSchedules((elem) => {
|
||||
addFromTemplateWithSchema(
|
||||
elem, "schedule-config",
|
||||
value.schedules, value.schema,
|
||||
value.max ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"schConfig": (_, value) => {
|
||||
let container = document.getElementById("schedules");
|
||||
container.dataset["settingsMax"] = value.max;
|
||||
|
||||
value.schedules.forEach((entries) => {
|
||||
addNode(fromSchema(entries, value.schema));
|
||||
});
|
||||
onConfig(value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function init() {
|
||||
variableListeners(listeners());
|
||||
groupSettingsOnAdd("schedules", () => {
|
||||
addNode();
|
||||
withSchedules((elem) => {
|
||||
variableListeners(listeners());
|
||||
groupSettingsOnAddElem(elem, () => {
|
||||
scheduleAdd(elem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { sendAction } from './connection.mjs';
|
||||
import { variableListeners } from './settings.mjs';
|
||||
|
||||
import {
|
||||
loadConfigTemplate,
|
||||
loadTemplate,
|
||||
mergeTemplate,
|
||||
} from './template.mjs';
|
||||
|
||||
import {
|
||||
onPanelTargetClick,
|
||||
@@ -15,199 +8,340 @@ import {
|
||||
} from './core.mjs';
|
||||
|
||||
import {
|
||||
fromSchema,
|
||||
initSelect,
|
||||
setChangedElement,
|
||||
setOriginalsFromValues,
|
||||
setSelectValue,
|
||||
variableListeners,
|
||||
} from './settings.mjs';
|
||||
|
||||
import {
|
||||
fromSchema,
|
||||
loadConfigTemplate,
|
||||
loadTemplate,
|
||||
mergeTemplate,
|
||||
NumberInput,
|
||||
} from './template.mjs';
|
||||
|
||||
/** @typedef {{name: string, units: number, type: number, index_global: number, description: string}} Magnitude */
|
||||
|
||||
/** @typedef {[number, string]} SupportedUnits */
|
||||
|
||||
const Magnitudes = {
|
||||
properties: {},
|
||||
errors: {},
|
||||
types: {},
|
||||
/** @type {Map<number, Magnitude>} */
|
||||
properties: new Map(),
|
||||
|
||||
/** @type {Map<number, string>} */
|
||||
errors: new Map(),
|
||||
|
||||
/** @type {Map<number, string>} */
|
||||
types: new Map(),
|
||||
|
||||
units: {
|
||||
names: {},
|
||||
supported: {}
|
||||
/** @type {Map<number, SupportedUnits[]>} */
|
||||
supported: new Map(),
|
||||
|
||||
/** @type {Map<number, string>} */
|
||||
names: new Map(),
|
||||
},
|
||||
typePrefix: {},
|
||||
prefixType: {}
|
||||
|
||||
/** @type {Map<number, string>} */
|
||||
typePrefix: new Map(),
|
||||
|
||||
/** @type {Map<string, number>} */
|
||||
prefixType: new Map(),
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Magnitude} magnitude
|
||||
* @param {string} name
|
||||
*/
|
||||
function magnitudeTypedKey(magnitude, name) {
|
||||
const prefix = Magnitudes.typePrefix[magnitude.type];
|
||||
const prefix = Magnitudes.typePrefix
|
||||
.get(magnitude.type);
|
||||
const index = magnitude.index_global;
|
||||
return `${prefix}${name}${index}`;
|
||||
}
|
||||
|
||||
function initModuleMagnitudes(data) {
|
||||
const targetId = `${data.prefix}-magnitudes`;
|
||||
/**
|
||||
* @param {string} prefix
|
||||
* @param {any[][]} values
|
||||
* @param {string[]} schema
|
||||
*/
|
||||
function initModuleMagnitudes(prefix, values, schema) {
|
||||
const container = document.getElementById(`${prefix}-magnitudes`);
|
||||
if (!container || container.childElementCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target = document.getElementById(targetId);
|
||||
if (target.childElementCount > 0) { return; }
|
||||
values.forEach((value) => {
|
||||
const magnitude = fromSchema(value, schema);
|
||||
|
||||
data.values.forEach((values) => {
|
||||
const entry = fromSchema(values, data.schema);
|
||||
const type = /** @type {!number} */
|
||||
(magnitude.type);
|
||||
const index_global = /** @type {!number} */
|
||||
(magnitude.index_global);
|
||||
const index_module = /** @type {!number} */
|
||||
(magnitude.index_module);
|
||||
|
||||
let line = loadConfigTemplate("module-magnitude");
|
||||
line.querySelector("label").textContent =
|
||||
`${Magnitudes.types[entry.type]} #${entry.index_global}`;
|
||||
line.querySelector("span").textContent =
|
||||
Magnitudes.properties[entry.index_global].description;
|
||||
const line = loadConfigTemplate("module-magnitude");
|
||||
|
||||
let input = line.querySelector("input");
|
||||
input.name = `${data.prefix}Magnitude`;
|
||||
input.value = entry.index_module;
|
||||
const label = /** @type {!HTMLLabelElement} */
|
||||
(line.querySelector("label"));
|
||||
label.textContent =
|
||||
`${Magnitudes.types.get(type) ?? "?"} #${magnitude.index_global}`;
|
||||
|
||||
const span = /** @type {!HTMLSpanElement} */
|
||||
(line.querySelector("span"));
|
||||
span.textContent =
|
||||
Magnitudes.properties.get(index_global)?.description ?? "";
|
||||
|
||||
const input = /** @type {!HTMLInputElement} */
|
||||
(line.querySelector("input"));
|
||||
input.name = `${prefix}Magnitude`;
|
||||
input.value = index_module.toString();
|
||||
input.dataset["original"] = input.value;
|
||||
|
||||
mergeTemplate(target, line);
|
||||
mergeTemplate(container, line);
|
||||
});
|
||||
}
|
||||
|
||||
function initMagnitudes(data) {
|
||||
data.types.values.forEach((cfg) => {
|
||||
const info = fromSchema(cfg, data.types.schema);
|
||||
Magnitudes.types[info.type] = info.name;
|
||||
Magnitudes.typePrefix[info.type] = info.prefix;
|
||||
Magnitudes.prefixType[info.prefix] = info.type;
|
||||
/**
|
||||
* @param {any} types
|
||||
* @param {any} errors
|
||||
* @param {any} units
|
||||
*/
|
||||
function initMagnitudes(types, errors, units) {
|
||||
/** @type {[number, string, string][]} */
|
||||
(types.values).forEach((value) => {
|
||||
const info = fromSchema(value, types.schema);
|
||||
|
||||
const type = /** @type {number} */(info.type);
|
||||
Magnitudes.types.set(type,
|
||||
/** @type {string} */(info.name));
|
||||
|
||||
const prefix = /** @type {string} */(info.prefix);
|
||||
Magnitudes.typePrefix.set(type, prefix);
|
||||
Magnitudes.prefixType.set(prefix, type);
|
||||
});
|
||||
|
||||
data.errors.values.forEach((cfg) => {
|
||||
const error = fromSchema(cfg, data.errors.schema);
|
||||
Magnitudes.errors[error.type] = error.name;
|
||||
/** @type {[number, string][]} */
|
||||
(errors.values).forEach((value) => {
|
||||
const error = fromSchema(value, errors.schema);
|
||||
Magnitudes.errors.set(
|
||||
/** @type {number} */(error.type),
|
||||
/** @type {string} */(error.name));
|
||||
});
|
||||
|
||||
data.units.values.forEach((cfg, id) => {
|
||||
const values = fromSchema(cfg, data.units.schema);
|
||||
values.supported.forEach(([type, name]) => {
|
||||
Magnitudes.units.names[type] = name;
|
||||
/** @type {SupportedUnits[][]} */
|
||||
(units.values).forEach((value, id) => {
|
||||
Magnitudes.units.supported.set(id, value);
|
||||
value.forEach(([type, name]) => {
|
||||
Magnitudes.units.names.set(type, name);
|
||||
});
|
||||
|
||||
Magnitudes.units.supported[id] = values.supported;
|
||||
});
|
||||
}
|
||||
|
||||
function initMagnitudesList(data, callbacks) {
|
||||
data.values.forEach((cfg, id) => {
|
||||
const magnitude = fromSchema(cfg, data.schema);
|
||||
const prettyName = Magnitudes.types[magnitude.type]
|
||||
.concat(" #").concat(parseInt(magnitude.index_global, 10));
|
||||
/**
|
||||
* @typedef {function(number, Magnitude): void} MagnitudeCallback
|
||||
* @param {any[]} values
|
||||
* @param {string[]} schema
|
||||
* @param {MagnitudeCallback[]} callbacks
|
||||
*/
|
||||
function initMagnitudesList(values, schema, callbacks) {
|
||||
values.forEach((value, id) => {
|
||||
const magnitude = fromSchema(value, schema);
|
||||
|
||||
const type = /** @type {number} */(magnitude.type);
|
||||
const prettyName =
|
||||
`${Magnitudes.types.get(type) ?? "?"} #${magnitude.index_global}`;
|
||||
|
||||
/** @type {Magnitude} */
|
||||
const result = {
|
||||
name: prettyName,
|
||||
units: magnitude.units,
|
||||
type: magnitude.type,
|
||||
index_global: magnitude.index_global,
|
||||
description: magnitude.description
|
||||
units: /** @type {number} */(magnitude.units),
|
||||
type: type,
|
||||
index_global: /** @type {number} */(magnitude.index_global),
|
||||
description: /** @type {string} */(magnitude.description),
|
||||
};
|
||||
|
||||
Magnitudes.properties[id] = result;
|
||||
Magnitudes.properties.set(id, result);
|
||||
callbacks.forEach((callback) => {
|
||||
callback(id, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {Magnitude} magnitude
|
||||
*/
|
||||
function createMagnitudeInfo(id, magnitude) {
|
||||
const container = document.getElementById("magnitudes");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = loadTemplate("magnitude-info");
|
||||
const label = info.querySelector("label");
|
||||
const line = loadTemplate("magnitude-info");
|
||||
|
||||
const label = /** @type {!HTMLLabelElement} */
|
||||
(line.querySelector("label"));
|
||||
label.textContent = magnitude.name;
|
||||
|
||||
const input = info.querySelector("input");
|
||||
input.dataset["id"] = id;
|
||||
input.dataset["type"] = magnitude.type;
|
||||
const input = /** @type {!HTMLInputElement} */
|
||||
(line.querySelector("input"));
|
||||
input.dataset["id"] = id.toString();
|
||||
input.dataset["type"] = magnitude.type.toString();
|
||||
|
||||
const description = info.querySelector(".magnitude-description");
|
||||
const info = /** @type {!HTMLSpanElement} */
|
||||
(line.querySelector(".magnitude-info"));
|
||||
info.style.display = "none";
|
||||
|
||||
const description = /** @type {!HTMLSpanElement} */
|
||||
(line.querySelector(".magnitude-description"));
|
||||
description.textContent = magnitude.description;
|
||||
|
||||
const extra = info.querySelector(".magnitude-info");
|
||||
extra.style.display = "none";
|
||||
|
||||
mergeTemplate(container, info);
|
||||
mergeTemplate(container, line);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @param {Magnitude} magnitude
|
||||
*/
|
||||
function createMagnitudeUnitSelector(id, magnitude) {
|
||||
// but, no need for the element when there's no choice
|
||||
const supported = Magnitudes.units.supported[id];
|
||||
if ((supported !== undefined) && (supported.length > 1)) {
|
||||
const line = loadTemplate("magnitude-units");
|
||||
line.querySelector("label").textContent =
|
||||
`${Magnitudes.types[magnitude.type]} #${magnitude.index_global}`;
|
||||
|
||||
const select = line.querySelector("select");
|
||||
select.setAttribute("name", magnitudeTypedKey(magnitude, "Units"));
|
||||
|
||||
const options = [];
|
||||
supported.forEach(([id, name]) => {
|
||||
options.push({id, name});
|
||||
});
|
||||
|
||||
initSelect(select, options);
|
||||
setSelectValue(select, magnitude.units);
|
||||
setOriginalsFromValues([select]);
|
||||
|
||||
const container = document.getElementById("magnitude-units");
|
||||
container.parentElement.classList.remove("maybe-hidden");
|
||||
mergeTemplate(container, line);
|
||||
const supported = Magnitudes.units.supported.get(id);
|
||||
if ((supported === undefined) || (!supported.length)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById("magnitude-units");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = loadTemplate("magnitude-units");
|
||||
|
||||
const label = /** @type {!HTMLLabelElement} */
|
||||
(line.querySelector("label"));
|
||||
label.textContent =
|
||||
`${Magnitudes.types.get(magnitude.type) ?? "?"} #${magnitude.index_global}`;
|
||||
|
||||
const select = /** @type {!HTMLSelectElement} */
|
||||
(line.querySelector("select"));
|
||||
select.setAttribute("name",
|
||||
magnitudeTypedKey(magnitude, "Units"));
|
||||
|
||||
/** @type {{id: number, name: string}[]} */
|
||||
const options = [];
|
||||
supported.forEach(([id, name]) => {
|
||||
options.push({id, name});
|
||||
});
|
||||
|
||||
initSelect(select, options);
|
||||
setSelectValue(select, magnitude.units);
|
||||
setOriginalsFromValues([select]);
|
||||
|
||||
container?.parentElement?.classList?.remove("maybe-hidden");
|
||||
mergeTemplate(container, line);
|
||||
}
|
||||
|
||||
function magnitudeSettingInfo(id, key) {
|
||||
/**
|
||||
* @typedef {{id: number, name: string, key: string, prefix: string, index_global: number}} SettingInfo
|
||||
* @param {number} id
|
||||
* @param {string} suffix
|
||||
* @returns {SettingInfo | null}
|
||||
*/
|
||||
function magnitudeSettingInfo(id, suffix) {
|
||||
const props = Magnitudes.properties.get(id);
|
||||
if (!props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prefix = Magnitudes.typePrefix.get(props.type);
|
||||
if (!prefix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const out = {
|
||||
id: id,
|
||||
name: Magnitudes.properties[id].name,
|
||||
prefix: `${Magnitudes.typePrefix[Magnitudes.properties[id].type]}`,
|
||||
index_global: `${Magnitudes.properties[id].index_global}`
|
||||
name: props.name,
|
||||
key: `${prefix}${suffix}${props.index_global}`,
|
||||
prefix: prefix,
|
||||
index_global: props.index_global,
|
||||
};
|
||||
|
||||
out.key = `${out.prefix}${key}${out.index_global}`;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {SettingInfo | null}
|
||||
*/
|
||||
function emonRatioInfo(id) {
|
||||
return magnitudeSettingInfo(id, "Ratio");
|
||||
}
|
||||
|
||||
function initMagnitudeTextSetting(containerId, id, keySuffix, value) {
|
||||
const template = loadTemplate("text-input");
|
||||
const input = template.querySelector("input");
|
||||
/**
|
||||
* @param {string} containerId
|
||||
* @param {number} id
|
||||
* @param {string} keySuffix
|
||||
* @param {number} value
|
||||
*/
|
||||
function initMagnitudeNumberSetting(containerId, id, keySuffix, value) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = magnitudeSettingInfo(id, keySuffix);
|
||||
input.id = info.key;
|
||||
input.name = input.id;
|
||||
input.value = value;
|
||||
setOriginalsFromValues([input]);
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = template.querySelector("label");
|
||||
label.textContent = info.name;
|
||||
label.htmlFor = input.id;
|
||||
container?.parentElement?.classList?.remove("maybe-hidden");
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
container.parentElement.classList.remove("maybe-hidden");
|
||||
mergeTemplate(container, template);
|
||||
}
|
||||
|
||||
function initMagnitudesRatio(id, value) {
|
||||
initMagnitudeTextSetting("emon-ratios", id, "Ratio", value);
|
||||
const template = new NumberInput();
|
||||
mergeTemplate(container, template.with(
|
||||
(label, input) => {
|
||||
label.textContent = info.name;
|
||||
label.htmlFor = input.id;
|
||||
|
||||
input.id = info.key;
|
||||
input.name = input.id;
|
||||
input.value = value.toString();
|
||||
|
||||
setOriginalsFromValues([input]);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
function initMagnitudesExpected(id) {
|
||||
// TODO: also display currently read value?
|
||||
const template = loadTemplate("emon-expected");
|
||||
const [expected, result] = template.querySelectorAll("input");
|
||||
const container = document.getElementById("emon-expected");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = emonRatioInfo(id);
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
expected.name += `${info.key}`;
|
||||
// TODO: also display currently read value?
|
||||
const template = loadTemplate("emon-expected");
|
||||
|
||||
const [expected, result] = template.querySelectorAll("input");
|
||||
expected.name += info.key;
|
||||
expected.id = expected.name;
|
||||
expected.dataset["id"] = info.id;
|
||||
expected.dataset["id"] = info.id.toString();
|
||||
|
||||
result.name += `${info.key}`;
|
||||
result.name += info.key;
|
||||
result.id = result.name;
|
||||
|
||||
const label = template.querySelector("label");
|
||||
const [label] = template.querySelectorAll("label");
|
||||
label.textContent = info.name;
|
||||
label.htmlFor = expected.id;
|
||||
|
||||
@@ -215,141 +349,214 @@ function initMagnitudesExpected(id) {
|
||||
styleVisible(`.emon-expected-${info.prefix}`, true)
|
||||
]);
|
||||
|
||||
mergeTemplate(document.getElementById("emon-expected"), template);
|
||||
mergeTemplate(container, template);
|
||||
}
|
||||
|
||||
function emonCalculateRatios() {
|
||||
const inputs = document.getElementById("emon-expected")
|
||||
.querySelectorAll(".emon-expected-input");
|
||||
const expected = document.getElementById("emon-expected")
|
||||
?.querySelectorAll("input.emon-expected-input");
|
||||
if (!expected) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputs.forEach((input) => {
|
||||
if (input.value.length) {
|
||||
sendAction("emon-expected", {
|
||||
id: parseInt(input.dataset["id"], 10),
|
||||
expected: parseFloat(input.value) });
|
||||
/** @type {NodeListOf<HTMLInputElement>} */
|
||||
(expected).forEach((input) => {
|
||||
if (!input.value || !input.dataset["id"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendAction("emon-expected", {
|
||||
id: parseInt(input.dataset["id"], 10),
|
||||
expected: parseFloat(input.value) });
|
||||
});
|
||||
}
|
||||
|
||||
function emonApplyRatios() {
|
||||
const results = document.getElementById("emon-expected")
|
||||
.querySelectorAll(".emon-expected-result");
|
||||
?.querySelectorAll("input.emon-expected-result");
|
||||
|
||||
results.forEach((result) => {
|
||||
if (result.value.length) {
|
||||
const ratio = document.getElementById(
|
||||
result.name.replace("result:", ""));
|
||||
ratio.value = result.value;
|
||||
setChangedElement(ratio);
|
||||
|
||||
result.value = "";
|
||||
|
||||
const expected = document.getElementById(
|
||||
result.name.replace("result:", "expected:"));
|
||||
expected.value = "";
|
||||
/** @type {NodeListOf<HTMLInputElement>} */
|
||||
(results).forEach((result) => {
|
||||
if (!result.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let next = result.name
|
||||
.replace("result:", "");
|
||||
|
||||
const ratio = document.getElementById(next);
|
||||
if (!(ratio instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ratio.value = result.value;
|
||||
setChangedElement(ratio);
|
||||
|
||||
result.value = "";
|
||||
|
||||
next = result.name
|
||||
.replace("result:", "expected:");
|
||||
const expected = document.getElementById(next);
|
||||
if (!(expected instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
expected.value = "";
|
||||
});
|
||||
|
||||
showPanelByName("sns");
|
||||
}
|
||||
|
||||
function initMagnitudesCorrection(id, value) {
|
||||
initMagnitudeTextSetting("magnitude-corrections", id, "Correction", value);
|
||||
}
|
||||
/**
|
||||
* @param {any[]} values
|
||||
* @param {string[]} schema
|
||||
*/
|
||||
function initMagnitudesSettings(values, schema) {
|
||||
values.forEach((value, id) => {
|
||||
const settings = fromSchema(value, schema);
|
||||
|
||||
function initMagnitudesSettings(data) {
|
||||
data.values.forEach((cfg, id) => {
|
||||
const settings = fromSchema(cfg, data.schema);
|
||||
|
||||
if (settings.Ratio !== null) {
|
||||
initMagnitudesRatio(id, settings.Ratio);
|
||||
if (typeof settings.Ratio === "number") {
|
||||
initMagnitudeNumberSetting(
|
||||
"emon-ratios", id,
|
||||
"Ratio", settings.Ratio);
|
||||
initMagnitudesExpected(id);
|
||||
}
|
||||
|
||||
if (settings.Correction !== null) {
|
||||
initMagnitudesCorrection(id, settings.Correction);
|
||||
if (typeof settings.Correction === "number") {
|
||||
initMagnitudeNumberSetting(
|
||||
"magnitude-corrections", id,
|
||||
"Correction", settings.Correction);
|
||||
}
|
||||
|
||||
let threshold = settings.ZeroThreshold;
|
||||
if (threshold === null) {
|
||||
threshold = NaN;
|
||||
const threshold =
|
||||
(typeof settings.ZeroThreshold === "number")
|
||||
? settings.ZeroThreshold :
|
||||
(typeof settings.ZeroThreshold === "string")
|
||||
? NaN : null;
|
||||
|
||||
if (typeof threshold === "number") {
|
||||
initMagnitudeNumberSetting(
|
||||
"magnitude-zero-thresholds", id,
|
||||
"ZeroThreshold", threshold);
|
||||
}
|
||||
|
||||
initMagnitudeTextSetting(
|
||||
"magnitude-zero-thresholds", id,
|
||||
"ZeroThreshold", threshold);
|
||||
if (typeof settings.MinDelta === "number") {
|
||||
initMagnitudeNumberSetting(
|
||||
"magnitude-min-deltas", id,
|
||||
"MinDelta", settings.MinDelta);
|
||||
}
|
||||
|
||||
initMagnitudeTextSetting(
|
||||
"magnitude-min-deltas", id,
|
||||
"MinDelta", settings.MinDelta);
|
||||
|
||||
initMagnitudeTextSetting(
|
||||
"magnitude-max-deltas", id,
|
||||
"MaxDelta", settings.MaxDelta);
|
||||
if (typeof settings.MaxDelta === "number") {
|
||||
initMagnitudeNumberSetting(
|
||||
"magnitude-max-deltas", id,
|
||||
"MaxDelta", settings.MaxDelta);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {HTMLInputElement | null}
|
||||
*/
|
||||
function magnitudeValueContainer(id) {
|
||||
return document.querySelector(`input[name='magnitude'][data-id='${id}']`);
|
||||
}
|
||||
|
||||
function updateMagnitudes(data) {
|
||||
data.values.forEach((cfg, id) => {
|
||||
if (!Magnitudes.properties[id]) {
|
||||
/**
|
||||
* @param {any[]} values
|
||||
* @param {string[]} schema
|
||||
*/
|
||||
function updateMagnitudes(values, schema) {
|
||||
values.forEach((value, id) => {
|
||||
const props = Magnitudes.properties.get(id);
|
||||
if (!props) {
|
||||
return;
|
||||
}
|
||||
|
||||
const magnitude = fromSchema(cfg, data.schema);
|
||||
const properties = Magnitudes.properties[id];
|
||||
properties.units = magnitude.units;
|
||||
|
||||
const units = Magnitudes.units.names[properties.units] || "";
|
||||
const input = magnitudeValueContainer(id);
|
||||
input.value = (0 !== magnitude.error)
|
||||
? Magnitudes.errors[magnitude.error]
|
||||
: (("nan" === magnitude.value)
|
||||
? ""
|
||||
: `${magnitude.value}${units}`);
|
||||
});
|
||||
}
|
||||
|
||||
function updateEnergy(data) {
|
||||
data.values.forEach((cfg) => {
|
||||
const energy = fromSchema(cfg, data.schema);
|
||||
if (!Magnitudes.properties[energy.id]) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (energy.saved.length) {
|
||||
const input = magnitudeValueContainer(energy.id);
|
||||
const info = input.parentElement.parentElement
|
||||
.querySelector(".magnitude-info");
|
||||
info.style.display = "inherit";
|
||||
info.textContent = energy.saved;
|
||||
const magnitude = fromSchema(value, schema);
|
||||
if (typeof magnitude.units === "number") {
|
||||
props.units = magnitude.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) ?? "";
|
||||
input.value = `${magnitude.value}${units}`;
|
||||
} else {
|
||||
input.value = "?";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any[]} values
|
||||
* @param {string[]} schema
|
||||
*/
|
||||
function updateEnergy(values, schema) {
|
||||
values.forEach((value) => {
|
||||
const energy = fromSchema(value, schema);
|
||||
if (typeof energy.id !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = magnitudeValueContainer(energy.id);
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const props = Magnitudes.properties.get(energy.id);
|
||||
if (!props) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof energy.saved !== "string" || !energy.saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = input?.parentElement?.parentElement
|
||||
?.querySelector(".magnitude-info");
|
||||
if (!(info instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
info.style.display = "inherit";
|
||||
info.textContent = energy.saved;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"magnitudes-init": (_, value) => {
|
||||
initMagnitudes(value);
|
||||
initMagnitudes(
|
||||
value.types, value.errors, value.units);
|
||||
},
|
||||
"magnitudes-module": (_, value) => {
|
||||
initModuleMagnitudes(value);
|
||||
initModuleMagnitudes(
|
||||
value.prefix, value.values, value.schema);
|
||||
},
|
||||
"magnitudes-list": (_, value) => {
|
||||
initMagnitudesList(value, [
|
||||
initMagnitudesList(value.values, value.schema, [
|
||||
createMagnitudeUnitSelector, createMagnitudeInfo]);
|
||||
},
|
||||
"magnitudes-settings": (_, value) => {
|
||||
initMagnitudesSettings(value);
|
||||
initMagnitudesSettings(value.values, value.schema);
|
||||
},
|
||||
"magnitudes": (_, value) => {
|
||||
updateMagnitudes(value);
|
||||
updateMagnitudes(value.values, value.schema);
|
||||
},
|
||||
"energy": (_, value) => {
|
||||
updateEnergy(value);
|
||||
updateEnergy(value.values, value.schema);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -358,9 +565,9 @@ export function init() {
|
||||
variableListeners(listeners());
|
||||
|
||||
document.querySelector(".button-emon-expected")
|
||||
.addEventListener("click", onPanelTargetClick);
|
||||
?.addEventListener("click", onPanelTargetClick);
|
||||
document.querySelector(".button-emon-expected-calculate")
|
||||
.addEventListener("click", emonCalculateRatios);
|
||||
?.addEventListener("click", emonCalculateRatios);
|
||||
document.querySelector(".button-emon-expected-apply")
|
||||
.addEventListener("click", emonApplyRatios);
|
||||
?.addEventListener("click", emonApplyRatios);
|
||||
}
|
||||
|
||||
@@ -31,28 +31,6 @@ function resetGroupPending(elem) {
|
||||
elem.dataset["settingsGroupPending"] = "";
|
||||
}
|
||||
|
||||
// TODO: note that we also include kv schema as 'data-settings-schema' on the container.
|
||||
// produce a 'set' and compare instead of just matching length?
|
||||
|
||||
/**
|
||||
* @param {string[]} source
|
||||
* @param {string[]} schema
|
||||
* @returns {{[k: string]: any}}
|
||||
*/
|
||||
export function fromSchema(source, schema) {
|
||||
if (schema.length !== source.length) {
|
||||
throw `Schema mismatch! Expected length ${schema.length} vs. ${source.length}`;
|
||||
}
|
||||
|
||||
/** @type {{[k: string]: string}} */
|
||||
let target = {};
|
||||
schema.forEach((key, index) => {
|
||||
target[key] = source[index];
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
// Right now, group additions happen from:
|
||||
// - WebSocket, likely to happen exactly once per connection through processData handler(s). Specific keys trigger functions that append into the container element.
|
||||
// - User input. Same functions are triggered, but with an additional event for the container element that causes most recent element to be marked as changed.
|
||||
@@ -270,7 +248,7 @@ function groupSettingsCheckMax(event) {
|
||||
const max = target.dataset["settingsMax"];
|
||||
const val = 1 + target.children.length;
|
||||
|
||||
if ((max !== undefined) && (parseInt(max, 10) < val)) {
|
||||
if ((max !== undefined) && (max !== "0") && (parseInt(max, 10) < val)) {
|
||||
alert(`Max number of ${target.id} has been reached (${val} out of ${max})`);
|
||||
return false;
|
||||
}
|
||||
@@ -279,21 +257,31 @@ function groupSettingsCheckMax(event) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} elementId
|
||||
* @param {HTMLElement} elem
|
||||
* @param {EventListener} listener
|
||||
*/
|
||||
export function groupSettingsOnAdd(elementId, listener) {
|
||||
document.getElementById(elementId)
|
||||
?.addEventListener("settings-group-add",
|
||||
(event) => {
|
||||
event.stopPropagation();
|
||||
if (!groupSettingsCheckMax(event)) {
|
||||
return;
|
||||
}
|
||||
export function groupSettingsOnAddElem(elem, listener) {
|
||||
elem.addEventListener("settings-group-add",
|
||||
(event) => {
|
||||
event.stopPropagation();
|
||||
if (!groupSettingsCheckMax(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
listener(event);
|
||||
onGroupSettingsEventAdd(event);
|
||||
});
|
||||
listener(event);
|
||||
onGroupSettingsEventAdd(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {EventListener} listener
|
||||
*/
|
||||
export function groupSettingsOnAdd(id, listener) {
|
||||
const elem = document.getElementById(id);
|
||||
if (elem) {
|
||||
groupSettingsOnAddElem(elem, listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1225,12 +1213,7 @@ export function pendingChanges() {
|
||||
return Settings.counters.changed > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO https://github.com/microsoft/TypeScript/issues/58969 ? at-import becomes 'unused' for some reason
|
||||
* @typedef {import("./question.mjs").QuestionWrapper} QuestionWrapper
|
||||
*/
|
||||
|
||||
/** @type {QuestionWrapper} */
|
||||
/** @type {import("./question.mjs").QuestionWrapper} */
|
||||
export function askSaveSettings(ask) {
|
||||
if (pendingChanges()) {
|
||||
return ask("There are pending changes to the settings, continue the operation without saving?");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template id="template-curtain-control">
|
||||
<div class="curtain-roller" id="curtainGetPicture"></div>
|
||||
<div class="curtain-roller" id="curtain-picture"></div>
|
||||
<div class="pure-button-group">
|
||||
<button type="button" class="pure-button pure-input-1-3 curtain-button button-curtain-close">CLOSE</button>
|
||||
<button type="button" class="pure-button pure-input-1-3 curtain-button button-curtain-pause">PAUSE</button>
|
||||
<button type="button" class="pure-button pure-input-1-3 curtain-button button-curtain-open">OPEN</button>
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<input type="range" list="tickmarks" min="0" max="100" class="slider pure-input-1 reverse-range" id="curtainSet">
|
||||
<input type="range" list="tickmarks" min="0" max="100" class="slider pure-input-1 reverse-range" id="curtain-set">
|
||||
<datalist id="tickmarks">
|
||||
<option value="0"></option>
|
||||
<option value="50"></option>
|
||||
|
||||
@@ -69,22 +69,21 @@ export function loadConfigTemplate(name) {
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @import { DisplayValue } from './settings.mjs'
|
||||
* @typedef {{[k: string]: DisplayValue}} TemplateConfig
|
||||
*/
|
||||
|
||||
/** @typedef {InputOrSelect | HTMLSpanElement} TemplateLineElement */
|
||||
|
||||
/**
|
||||
* @import { DisplayValue } from './settings.mjs'
|
||||
* @param {DocumentFragment} fragment
|
||||
* @param {number} id
|
||||
* @param {{[k: string]: DisplayValue}} cfg
|
||||
* @param {TemplateConfig} cfg
|
||||
*/
|
||||
export function fillTemplateFromCfg(fragment, id, cfg) {
|
||||
let local = {"template-id": id};
|
||||
if (cfg === undefined) {
|
||||
cfg = {};
|
||||
}
|
||||
|
||||
Object.assign(local, cfg);
|
||||
cfg = local;
|
||||
export function fillTemplateFromCfg(fragment, id, cfg = {}) {
|
||||
const local = {"template-id": id};
|
||||
cfg = Object.assign({}, local, cfg);
|
||||
|
||||
for (let elem of /** @type {NodeListOf<TemplateLineElement>} */(fragment.querySelectorAll("input,select,span"))) {
|
||||
const key =
|
||||
@@ -130,10 +129,100 @@ export function mergeTemplate(target, template) {
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {string} name
|
||||
* @param {any} cfg
|
||||
* @param {TemplateConfig} cfg
|
||||
*/
|
||||
export function addFromTemplate(container, name, cfg) {
|
||||
const fragment = loadConfigTemplate(name);
|
||||
fillTemplateFromCfg(fragment, container.childElementCount, cfg);
|
||||
mergeTemplate(container, fragment);
|
||||
}
|
||||
|
||||
// TODO: note that we also include kv schema as 'data-settings-schema' on the container.
|
||||
// produce a 'set' and compare instead of just matching length?
|
||||
|
||||
/**
|
||||
* @param {DisplayValue[]} values
|
||||
* @param {string[]} schema
|
||||
* @returns {TemplateConfig}
|
||||
*/
|
||||
export function fromSchema(values, schema) {
|
||||
if (schema.length !== values.length) {
|
||||
throw `Schema mismatch! Expected length ${schema.length} vs. ${values.length}`;
|
||||
}
|
||||
|
||||
/** @type {{[k: string]: any}} */
|
||||
const out = {};
|
||||
schema.forEach((key, index) => {
|
||||
out[key] = values[index];
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DisplayValue[][]} source
|
||||
* @param {string[]} schema
|
||||
* @returns {TemplateConfig[]}
|
||||
*/
|
||||
export function prepareFromSchema(source, schema) {
|
||||
return source.map((values) => fromSchema(values, schema));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
* @param {string} name
|
||||
* @param {DisplayValue[][]} entries
|
||||
* @param {string[]} schema
|
||||
* @param {number} max
|
||||
*/
|
||||
export function addFromTemplateWithSchema(container, name, entries, schema, max = 0) {
|
||||
const prepared = prepareFromSchema(entries, schema);
|
||||
if (!prepared) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (max > 0) {
|
||||
container.dataset["settingsMax"] = max.toString();
|
||||
}
|
||||
|
||||
prepared.forEach((cfg) => {
|
||||
addFromTemplate(container, name, cfg);
|
||||
});
|
||||
}
|
||||
|
||||
export class BaseInput {
|
||||
/** @param {string} name */
|
||||
constructor(name) {
|
||||
this.fragment = loadConfigTemplate(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(HTMLLabelElement, HTMLInputElement): void} callback
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
with(callback) {
|
||||
const out = document.createDocumentFragment();
|
||||
out.appendChild(this.fragment.cloneNode(true));
|
||||
|
||||
const root = /** @type {!HTMLDivElement} */
|
||||
(out.children[0]);
|
||||
|
||||
callback(
|
||||
/** @type {!HTMLLabelElement} */(root.children[0]),
|
||||
/** @type {!HTMLInputElement} */(root.children[1]));
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
export class TextInput extends BaseInput {
|
||||
constructor() {
|
||||
super("text-input");
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberInput extends BaseInput {
|
||||
constructor() {
|
||||
super("number-input");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,27 +2,41 @@ import { askAndCall } from './question.mjs';
|
||||
import { askSaveSettings } from './settings.mjs';
|
||||
import { sendAction } from './connection.mjs';
|
||||
|
||||
function checkTempRange(event) {
|
||||
const min = document.getElementById("tempRangeMinInput");
|
||||
const max = document.getElementById("tempRangeMaxInput");
|
||||
|
||||
if (event.target.id === max.id) {
|
||||
/**
|
||||
* @param {HTMLInputElement} min
|
||||
* @param {HTMLInputElement} max
|
||||
*/
|
||||
function checkTempMax(min, max) {
|
||||
return function() {
|
||||
const maxValue = parseInt(max.value, 10) - 1;
|
||||
if (parseInt(min.value, 10) > maxValue) {
|
||||
min.value = maxValue;
|
||||
}
|
||||
} else {
|
||||
const minValue = parseInt(min.value, 10) + 1;
|
||||
if (parseInt(max.value, 10) < minValue) {
|
||||
max.value = minValue;
|
||||
min.value = maxValue.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLInputElement} min
|
||||
* @param {HTMLInputElement} max
|
||||
*/
|
||||
function checkTempMin(min, max) {
|
||||
return function() {
|
||||
const minValue = parseInt(min.value, 10) + 1;
|
||||
if (parseInt(max.value, 10) < minValue) {
|
||||
max.value = minValue.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import("./question.mjs").QuestionWrapper} */
|
||||
function askResetCounters(ask) {
|
||||
return ask("Are you sure you want to reset burning counters?");
|
||||
}
|
||||
|
||||
function onResetCounters() {
|
||||
const questions = [
|
||||
askSaveSettings,
|
||||
(ask) => ask("Are you sure you want to reset burning counters?")
|
||||
askResetCounters,
|
||||
];
|
||||
|
||||
askAndCall(questions, () => {
|
||||
@@ -32,9 +46,16 @@ function onResetCounters() {
|
||||
|
||||
export function init() {
|
||||
document.querySelector(".button-thermostat-reset-counters")
|
||||
.addEventListener("click", onResetCounters);
|
||||
document.getElementById("tempRangeMaxInput")
|
||||
.addEventListener("change", checkTempRange);
|
||||
document.getElementById("tempRangeMinInput")
|
||||
.addEventListener("change", checkTempRange);
|
||||
?.addEventListener("click", onResetCounters);
|
||||
|
||||
const [min, max] =
|
||||
["Min", "Max"]
|
||||
.map((x) => `tempRange${x}Input`)
|
||||
.map((x) => document.getElementById(x))
|
||||
.filter((x) => x instanceof HTMLInputElement);
|
||||
|
||||
if (min && max) {
|
||||
min.addEventListener("change", checkTempMin(min, max));
|
||||
max.addEventListener("change", checkTempMax(min, max));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createNodeList } from './relay.mjs';
|
||||
import { variableListeners } from './settings.mjs';
|
||||
|
||||
/** @returns {import('./settings.mjs').KeyValueListeners} */
|
||||
function listeners() {
|
||||
return {
|
||||
"tspkRelays": (_, value) => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { isChangedElement } from './settings.mjs';
|
||||
|
||||
/**
|
||||
* @param {string} password
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function validatePassword(password) {
|
||||
// http://www.the-art-of-web.com/javascript/validate-password/
|
||||
// at least one lowercase and one uppercase letter or number
|
||||
@@ -17,24 +21,34 @@ export function validatePassword(password) {
|
||||
&& Pattern.test(password));
|
||||
}
|
||||
|
||||
// Try to validate 'adminPass{0,1}', searching the first form containing both.
|
||||
// In case it's default webMode, avoid checking things when both fields are empty (`required === false`)
|
||||
export function validateFormsPasswords(forms, required) {
|
||||
let [passwords] = Array.from(forms).filter(
|
||||
form => form.elements.adminPass0 && form.elements.adminPass1);
|
||||
|
||||
if (passwords) {
|
||||
let first = passwords.elements.adminPass0;
|
||||
let second = passwords.elements.adminPass1;
|
||||
/**
|
||||
* Try to validate 'adminPass{0,1}', searching the first form containing both.
|
||||
* In case it's default webMode, avoid checking things when both fields are empty (`required === false`)
|
||||
* @param {HTMLFormElement[]} forms
|
||||
* @param {{required?: boolean, strict?: boolean}} options
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function validateFormsPasswords(forms, {required = true, strict = true} = {}) {
|
||||
const [first, second] = Array.from(forms)
|
||||
.flatMap((x) => {
|
||||
return [
|
||||
x.elements.namedItem("adminPass0"),
|
||||
x.elements.namedItem("adminPass1"),
|
||||
];
|
||||
})
|
||||
.filter((x) => x instanceof HTMLInputElement);
|
||||
|
||||
if (first && second) {
|
||||
if (!required && !first.value.length && !second.value.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let firstValid = first.checkValidity() && validatePassword(first.value);
|
||||
let secondValid = second.checkValidity() && validatePassword(second.value);
|
||||
const firstValid = first.checkValidity()
|
||||
&& (!strict || validatePassword(first.value));
|
||||
const secondValid = second.checkValidity()
|
||||
&& (!strict || validatePassword(second.value));
|
||||
if (firstValid && secondValid) {
|
||||
if (first.value === second.value) {
|
||||
if (first.value !== second.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -49,8 +63,11 @@ export function validateFormsPasswords(forms, required) {
|
||||
}
|
||||
|
||||
|
||||
// Same as above, but only applies to the general settings page.
|
||||
// Find the first available form that contains 'hostname' input
|
||||
/**
|
||||
* Same as above, but only applies to the general settings page.
|
||||
* Find the first available form that contains 'hostname' input
|
||||
* @param {HTMLFormElement[]} forms
|
||||
*/
|
||||
export function validateFormsHostname(forms) {
|
||||
// per. [RFC1035](https://datatracker.ietf.org/doc/html/rfc1035)
|
||||
// Hostname may contain:
|
||||
@@ -58,7 +75,9 @@ export function validateFormsHostname(forms) {
|
||||
// - the digits '0' through '9', and the hyphen.
|
||||
// Hostname labels cannot begin or end with a hyphen.
|
||||
// No other symbols, punctuation characters, or blank spaces are permitted.
|
||||
let [hostname] = Array.from(forms).filter(form => form.elements.hostname);
|
||||
const [hostname] = Array.from(forms)
|
||||
.flatMap(form => form.elements.namedItem("hostname"))
|
||||
.filter((x) => x instanceof HTMLInputElement);
|
||||
if (!hostname) {
|
||||
return true;
|
||||
}
|
||||
@@ -66,17 +85,20 @@ export function validateFormsHostname(forms) {
|
||||
// Validation pattern is attached to the element itself, so just check that.
|
||||
// (and, we also re-use the hostname for fallback SSID, thus limited to 1...32 chars instead of 1...63)
|
||||
|
||||
hostname = hostname.elements.hostname;
|
||||
let result = hostname.value.length
|
||||
const result = (hostname.value.length > 0)
|
||||
&& (!isChangedElement(hostname) || hostname.checkValidity());
|
||||
|
||||
if (!result) {
|
||||
alert("Hostname cannot be empty and may only contain the ASCII letters ('A' through 'Z' and 'a' through 'z'), the digits '0' through '9', and the hyphen ('-')! They can neither start or end with an hyphen.");
|
||||
alert(`Hostname cannot be empty and may only contain the ASCII letters ('A' through 'Z' and 'a' through 'z'),
|
||||
the digits '0' through '9', and the hyphen ('-')! They can neither start or end with an hyphen.`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLFormElement[]} forms
|
||||
*/
|
||||
export function validateForms(forms) {
|
||||
return validateFormsPasswords(forms) && validateFormsHostname(forms);
|
||||
return validateFormsPasswords(forms)
|
||||
&& validateFormsHostname(forms);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,51 @@
|
||||
import {
|
||||
fromSchema,
|
||||
groupSettingsOnAdd,
|
||||
groupSettingsOnAddElem,
|
||||
variableListeners,
|
||||
} from './settings.mjs';
|
||||
|
||||
import { addFromTemplate } from './template.mjs';
|
||||
import { addFromTemplate, addFromTemplateWithSchema } from './template.mjs';
|
||||
import { moreElem } from './core.mjs';
|
||||
import { sendAction } from './connection.mjs';
|
||||
|
||||
/**
|
||||
* @param {boolean?} showMore
|
||||
* @param {any?} cfg
|
||||
*/
|
||||
function addNode(showMore = false, cfg = undefined) {
|
||||
const container = document.getElementById("networks");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
/** @param {function(HTMLElement): void} callback */
|
||||
function withNetworks(callback) {
|
||||
callback(/** @type {!HTMLElement} */
|
||||
(document.getElementById("networks")));
|
||||
}
|
||||
|
||||
addFromTemplate(container, "network-config", cfg);
|
||||
if (showMore && container.lastChild instanceof HTMLElement) {
|
||||
moreElem(container.lastChild)
|
||||
/**
|
||||
* @param {HTMLElement} elem
|
||||
*/
|
||||
function lastMoreElem(elem) {
|
||||
if (elem.lastChild instanceof HTMLElement) {
|
||||
moreElem(elem.lastChild)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
*/
|
||||
function onConfig(value) {
|
||||
withNetworks((elem) => {
|
||||
addFromTemplateWithSchema(
|
||||
elem, "network-config",
|
||||
value.networks, value.schema,
|
||||
value.max ?? 0);
|
||||
lastMoreElem(elem);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elem
|
||||
*/
|
||||
function networkAdd(elem) {
|
||||
addFromTemplate(elem, "network-config", {});
|
||||
}
|
||||
|
||||
/** @param {function(HTMLTableElement): void} callback */
|
||||
function withTable(callback) {
|
||||
const table = document.getElementById("scanResult");
|
||||
if (!(table instanceof HTMLTableElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(table);
|
||||
callback(/** @type {!HTMLTableElement} */
|
||||
(document.getElementById("scanResult")));
|
||||
}
|
||||
|
||||
/** @param {function(HTMLInputElement): void} callback */
|
||||
@@ -50,10 +64,9 @@ function buttonDisabled(value) {
|
||||
|
||||
/** @param {boolean} value */
|
||||
function loadingDisplay(value) {
|
||||
const loading = document.querySelector("div.scan.loading");
|
||||
if (loading instanceof HTMLElement) {
|
||||
loading.style.display = value ? "table" : "none";
|
||||
}
|
||||
const loading = (/** @type {!HTMLDivElement} */
|
||||
(document.querySelector("div.scan.loading")));
|
||||
loading.style.display = value ? "table" : "none";
|
||||
}
|
||||
|
||||
/** @param {string[]} values */
|
||||
@@ -90,32 +103,12 @@ function scanStart(event) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./settings.mjs').KeyValueListeners } KeyValueListeners
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {KeyValueListeners}
|
||||
* @returns {import('./settings.mjs').KeyValueListeners}
|
||||
*/
|
||||
function listeners() {
|
||||
return {
|
||||
"wifiConfig": (_, value) => {
|
||||
const container = document.getElementById("networks");
|
||||
if (!(container instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.max !== undefined) {
|
||||
container.dataset["settingsMax"] = value.max;
|
||||
}
|
||||
|
||||
const networks = value.networks;
|
||||
if (!Array.isArray(networks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
networks.forEach((entries) => {
|
||||
addNode(false, fromSchema(entries, value.schema));
|
||||
});
|
||||
onConfig(value);
|
||||
},
|
||||
"scanResult": (_, value) => {
|
||||
scanResult(value);
|
||||
@@ -124,13 +117,14 @@ function listeners() {
|
||||
}
|
||||
|
||||
export function init() {
|
||||
variableListeners(listeners());
|
||||
|
||||
groupSettingsOnAdd("networks", () => {
|
||||
addNode();
|
||||
});
|
||||
|
||||
withButton((button) => {
|
||||
button.addEventListener("click", scanStart);
|
||||
withNetworks((elem) => {
|
||||
variableListeners(listeners());
|
||||
// TODO: as event arg?
|
||||
groupSettingsOnAddElem(elem, () => {
|
||||
networkAdd(elem);
|
||||
});
|
||||
withButton((button) => {
|
||||
button.addEventListener("click", scanStart);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user