Dingtian input HTTP-push to backend (no polling)
The device pushes button events to the backend via its Input Link URL feature;
the backend decides. No polling — the chosen entry architecture.
packages/devices:
- dingtian driver: configureInputPush() writes the device's input_link_url
config (per-input server/port/path, en=1, active-LOW, plain HTTP) so each
input HTTP-GETs the backend on press/release. Extracted #readConfig/#writeConfig
(with the required command:setconfig injection + post-write reset tolerance).
apps/server:
- routes/devices.ts: public GET/POST
/api/devices/dingtian/:deviceId/input/:n/{on,off} — translates a device push
into an internal device event. Not behind cookie/CSRF (machine call from the
device); trust comes from the signed event log, not this request.
- device-events.ts: internal EventEmitter bus so the entry flow subscribes to
input events without coupling to HTTP. Wired into the server.
Verified on hardware: configured the device, then real presses on all 4 inputs
pushed to the backend (input N on+off, source = device IP). No polling.
wiki: device-input-flow concept (path + trust model for the flat/no-VLAN
network); dingtian-relay updated; index + log.
This commit is contained in:
@@ -202,9 +202,7 @@ class DingtianController
|
||||
async checkPreconditions(): Promise<PreconditionResult> {
|
||||
let cfg: Record<string, unknown>;
|
||||
try {
|
||||
cfg = JSON.parse(
|
||||
await configApi(this.#host, this.#httpPort, "/api/v2/config.cgi", "GET", null, this.#timeout),
|
||||
);
|
||||
cfg = await this.#readConfig();
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -221,8 +219,7 @@ class DingtianController
|
||||
}
|
||||
|
||||
async fixPreconditions(): Promise<PreconditionResult> {
|
||||
const raw = await configApi(this.#host, this.#httpPort, "/api/v2/config.cgi", "GET", null, this.#timeout);
|
||||
const cfg = JSON.parse(raw) as Record<string, unknown>;
|
||||
const cfg = await this.#readConfig();
|
||||
if (this.#linkDisabled(cfg)) return { ok: true, issues: [] };
|
||||
|
||||
// Disable the master flag AND clear the per-input action maps.
|
||||
@@ -232,6 +229,54 @@ class DingtianController
|
||||
ilr.on_action_on = (ilr.on_action_on as unknown[]).map(() => []);
|
||||
}
|
||||
|
||||
await this.#writeConfig(cfg);
|
||||
return this.checkPreconditions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the device to HTTP-push input (button) events to our backend —
|
||||
* the "Input Link URL" feature. Each input N calls `${pathBase}/<N>/on` (and
|
||||
* `/off`) on the given host:port via GET. Enables the feature and disables TLS
|
||||
* (plain HTTP to the local backend). Replaces polling.
|
||||
*/
|
||||
async configureInputPush(opts: {
|
||||
host: string;
|
||||
port: number;
|
||||
pathBase: string; // e.g. "/api/devices/dingtian/<deviceId>/input"
|
||||
}): Promise<void> {
|
||||
const cfg = await this.#readConfig();
|
||||
const ilu = cfg.input_link_url as Record<string, unknown>;
|
||||
const n = Number((ilu.cnt as number) ?? this.#channels);
|
||||
const fill = (v: unknown) => Array.from({ length: n }, () => v);
|
||||
|
||||
ilu.en = 1;
|
||||
ilu.active_level = fill(0); // active-LOW (matches this board's wiring)
|
||||
ilu.tls = fill(0);
|
||||
ilu.auth = fill(0);
|
||||
ilu.server = fill(opts.host);
|
||||
ilu.port = fill(opts.port);
|
||||
ilu.user = fill("");
|
||||
ilu.pass = fill("");
|
||||
ilu.on_method = fill(0); // GET
|
||||
ilu.off_method = fill(0);
|
||||
ilu.on_path = Array.from({ length: n }, (_, i) => `${opts.pathBase}/${i + 1}/on`);
|
||||
ilu.off_path = Array.from({ length: n }, (_, i) => `${opts.pathBase}/${i + 1}/off`);
|
||||
ilu.on_body = fill("");
|
||||
ilu.off_body = fill("");
|
||||
|
||||
await this.#writeConfig(cfg);
|
||||
}
|
||||
|
||||
// --- config api internals ----------------------------------------------
|
||||
|
||||
async #readConfig(): Promise<Record<string, unknown>> {
|
||||
const raw = await configApi(this.#host, this.#httpPort, "/api/v2/config.cgi", "GET", null, this.#timeout);
|
||||
return JSON.parse(raw) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Write full config back. Injects the required `command:setconfig` and
|
||||
* tolerates the device resetting on apply. */
|
||||
async #writeConfig(cfg: Record<string, unknown>): Promise<void> {
|
||||
// The set endpoint requires `"command":"setconfig"` injected after `status`
|
||||
// (the GET payload omits it). Rebuild preserving node order, command second.
|
||||
const out: Record<string, unknown> = {};
|
||||
@@ -242,7 +287,7 @@ class DingtianController
|
||||
if (!("command" in out)) out.command = "setconfig";
|
||||
|
||||
// Device resets/applies after a write, so the connection may drop — that's
|
||||
// success, not failure. Swallow the post-write reset and verify by re-reading.
|
||||
// success, not failure. Swallow the post-write reset.
|
||||
try {
|
||||
await configApi(
|
||||
this.#host,
|
||||
@@ -253,11 +298,9 @@ class DingtianController
|
||||
this.#timeout,
|
||||
);
|
||||
} catch {
|
||||
// device likely reset on apply — ignore and verify below
|
||||
// device likely reset on apply
|
||||
}
|
||||
// Give the device a moment to apply, then re-read to confirm.
|
||||
await new Promise((r) => setTimeout(r, 4000));
|
||||
return this.checkPreconditions();
|
||||
}
|
||||
|
||||
#linkDisabled(cfg: Record<string, unknown>): boolean {
|
||||
|
||||
Reference in New Issue
Block a user