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
87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
import { ClosureComponent } from "mithril";
|
|
import { m } from "../components.ts";
|
|
import * as taskQueue from "../task-queue.ts";
|
|
import * as store from "../store.ts";
|
|
import * as notifications from "../notifications.ts";
|
|
import Expression from "../../lib/common/expression.ts";
|
|
import { FlatDevice } from "../../lib/ui/db.ts";
|
|
|
|
interface Attrs {
|
|
device: FlatDevice;
|
|
parameters: Record<string, Expression>;
|
|
}
|
|
|
|
const component: ClosureComponent<Attrs> = () => {
|
|
return {
|
|
view: (vnode) => {
|
|
const device = vnode.attrs.device;
|
|
|
|
return m(
|
|
"button",
|
|
{
|
|
class:
|
|
"px-2.5 py-1.5 border border-transparent text-xs font-medium rounded-sm shadow-xs text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
|
title: "Initiate session and refresh basic parameters",
|
|
onclick: (e) => {
|
|
e.target.disabled = true;
|
|
const params = Object.values(vnode.attrs["parameters"])
|
|
.map((exp) => {
|
|
if (exp instanceof Expression.Parameter)
|
|
return exp.path.toString();
|
|
return null;
|
|
})
|
|
.filter((exp) => !!exp);
|
|
|
|
const task = {
|
|
name: "getParameterValues",
|
|
parameterNames: params,
|
|
device: device["DeviceID.ID"] as string,
|
|
};
|
|
|
|
taskQueue
|
|
.commit(
|
|
[task],
|
|
(deviceId, err, connectionRequestStatus, tasks2) => {
|
|
if (err) {
|
|
notifications.push("error", `${deviceId}: ${err.message}`);
|
|
return;
|
|
}
|
|
|
|
for (const t of tasks2)
|
|
if (t.status === "stale") taskQueue.deleteTask(t);
|
|
|
|
if (connectionRequestStatus !== "OK") {
|
|
notifications.push(
|
|
"error",
|
|
`${deviceId}: ${connectionRequestStatus}`,
|
|
);
|
|
} else if (tasks2[0].status === "stale") {
|
|
notifications.push(
|
|
"error",
|
|
`${deviceId}: No contact from device`,
|
|
);
|
|
} else if (tasks2[0].status === "fault") {
|
|
notifications.push("error", `${deviceId}: Refresh faulted`);
|
|
} else {
|
|
notifications.push("success", `${deviceId}: Summoned`);
|
|
}
|
|
},
|
|
)
|
|
.then(() => {
|
|
e.target.disabled = false;
|
|
store.setTimestamp(Date.now());
|
|
})
|
|
.catch((err) => {
|
|
e.target.disabled = false;
|
|
notifications.push("error", err.message);
|
|
});
|
|
},
|
|
},
|
|
"Summon",
|
|
);
|
|
},
|
|
};
|
|
};
|
|
|
|
export default component;
|