refactor: replace array-based expressions with typed class hierarchy

Replace the old Lisp-like array expression representation
(["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in
an abstract Expression class (Literal, Parameter, Binary, Unary,
FunctionCall, Conditional). This gives compile-time type safety
to expression construction, evaluation, and pattern matching.

Key changes:
- New lib/common/expression.ts defines the class hierarchy with
  map/evaluate/toString methods
- Rewrite parser as hand-rolled recursive descent, removing the
  parsimmon dependency
- Extend Path with a colon separator to unify parameter identity
  and attributes into a single address space (e.g.
  "Device.WiFi.SSID.1.Name:writable"). The colon field tracks
  how many trailing segments are attribute qualifiers. PathSet
  rewritten with separate param/attr segment indexes
- Flatten device attribute representation in the UI to
  colon-delimited keys mirroring the new path syntax
This commit is contained in:
Zaid Humaimidi committed 2026-03-12 14:58:20 -07:00
1 parent 75908e624f
commit 6f3ce6f7c8
71 files changed
+4045 -3449

No files matched your search

+42 -48
View File
@@ -2,54 +2,52 @@ import { ClosureComponent, Component, VnodeDOM } from "mithril";
import { m } from "../components.ts";
import * as taskQueue from "../task-queue.ts";
import * as store from "../store.ts";
import { evaluate } from "../../lib/common/expression/util.ts";
import Expression, { Value } from "../../lib/common/expression.ts";
import memoize from "../../lib/common/memoize.ts";
import timeAgo from "../timeago.ts";
import { icon } from "../tailwind-utility-components.ts";
const evaluateParam = memoize((exp, obj, now: number) => {
let timestamp = now;
exp = evaluate(exp, null, now, (e) => {
if (!Array.isArray(e)) return e;
for (let i = 1; i < e.length; ++i) {
if (
Array.isArray(e[i]) &&
e[i][0] === "PARAM" &&
!Array.isArray(e[i][1])
) {
let v = null;
const p = obj[e[i][1]];
if (p?.value) {
v = p.value[0];
timestamp = Math.min(timestamp, p.valueTimestamp);
const evaluateParam = memoize(
(
exp: Expression,
obj: any,
now: number,
): { value: Value; timestamp: number; parameter: string } => {
let timestamp = now;
const valueMap: Map<Expression.Literal, string> = new Map();
const lit = exp.evaluate((e): Expression.Literal => {
if (e instanceof Expression.Literal) return e;
if (e instanceof Expression.Parameter) {
let v = obj[e.path.toString()];
if (v) {
timestamp = Math.min(
timestamp,
obj[e.path.toString() + ":valueTimestamp"] ?? 0,
);
const t = obj[e.path.toString() + ":type"];
if (t === "xsd:dateTime" && typeof v === "number")
v = new Date(v).toLocaleString();
const val = new Expression.Literal(v);
valueMap.set(val, e.path.toString());
return val;
}
} else if (e instanceof Expression.FunctionCall) {
if (e.name === "NOW") return new Expression.Literal(now);
else if (e.name === "DATE_STRING") {
const v = e.args[0];
if (v instanceof Expression.Literal) {
return new Expression.Literal(
new Date(v.value as string | number).toLocaleString(),
);
}
}
e = e.slice();
e[i] = v;
}
}
if (e[0] === "FUNC" && e[1] === "DATE_STRING" && !Array.isArray(e[2]))
return new Date(e[2]).toLocaleString();
return e;
});
return new Expression.Literal(null);
});
let parameter = null;
let value = null;
if (!Array.isArray(exp)) {
value = exp;
} else if (exp[0] === "PARAM") {
const p = obj[exp[1]];
if (p?.value) {
timestamp = p.valueTimestamp;
value = p.value[0];
parameter = exp[1];
if (p.value[1] === "xsd:dateTime" && typeof value === "number")
value = new Date(value).toLocaleString();
}
}
return { value, timestamp, parameter };
});
return { value: lit.value, timestamp, parameter: valueMap.get(lit) };
},
);
const component: ClosureComponent = (): Component => {
return {
@@ -65,7 +63,7 @@ const component: ClosureComponent = (): Component => {
if (value == null) return null;
let edit;
if (device[parameter]?.writable) {
if (device[parameter + ":writable"]) {
edit = m(
"button",
{
@@ -73,13 +71,9 @@ const component: ClosureComponent = (): Component => {
onclick: () => {
taskQueue.stageSpv({
name: "setParameterValues",
devices: [device["DeviceID.ID"].value[0]],
devices: [device["DeviceID.ID"]],
parameterValues: [
[
parameter,
device[parameter].value[0],
device[parameter].value[1],
],
[parameter, device[parameter], device[parameter + ":type"]],
],
});
},