Files
genieacs/ui/components/parameter.ts
T
Zaid Humaimidi 6f3ce6f7c8 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
2026-03-12 14:58:20 -07:00

111 lines
3.4 KiB
TypeScript

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 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: 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(),
);
}
}
}
return new Expression.Literal(null);
});
return { value: lit.value, timestamp, parameter: valueMap.get(lit) };
},
);
const component: ClosureComponent = (): Component => {
return {
view: (vnode) => {
const device = vnode.attrs["device"];
const { value, timestamp, parameter } = evaluateParam(
vnode.attrs["parameter"],
device,
store.getTimestamp() + store.getClockSkew(),
);
if (value == null) return null;
let edit;
if (device[parameter + ":writable"]) {
edit = m(
"button",
{
title: "Edit parameter value",
onclick: () => {
taskQueue.stageSpv({
name: "setParameterValues",
devices: [device["DeviceID.ID"]],
parameterValues: [
[parameter, device[parameter], device[parameter + ":type"]],
],
});
},
},
m(icon, {
name: "edit",
class: "inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
}),
);
}
const el = m("long-text", { text: `${value}` });
return m(
"span.inline-flex overflow-hidden align-top",
{
onmouseover: (e) => {
e.redraw = false;
// Don't update any child element
if (e.target === (el as VnodeDOM).dom) {
const now = Date.now() + store.getClockSkew();
const localeString = new Date(timestamp).toLocaleString();
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
}
},
},
m("span.truncate", el),
edit,
);
},
};
};
export default component;