Files
parking_solution/packages/devices/src/drivers/access-dingtian.test.ts
T
julian 4418594af0
Build desktop / desktop (push) Successful in 4m16s
Build & push images / images (push) Successful in 2m43s
CI / check (push) Successful in 38s
refactor(setup): unify controller I/O — event-driven relays[] + generic inputs[]
The controller new/edit modal hardcoded both its outputs and its inputs, so an
operator could neither add a generic event-driven relay nor a free-standing input
(e.g. a second radar at the exit). This unifies both into symmetric, first-class
lists. Behaviour for existing booths is unchanged (back-compat, no DB migration).

Outputs — one event→action relays[] list:
- A relay is "when EVENT X happens, do its action": entry/exit/both pulse a
  barrier; a new `radarAlert` event drives a non-barrier alert lamp (blink while
  its trigger input is active, SOLID once the camera confirms a car).
- Dropped the separate config.buttonLight block — the lamp is just a relays[] row
  with direction:"radarAlert" (triggerInput + blink cadence). `alertRelaysOf()`
  replaces `buttonLightOf()`; ButtonLightController keeps its proven 3-state
  machine (serialized UDP, fail-OFF, hot-reload), now keyed per controllerId:relay
  so several alert lamps on one controller run independently. Every barrier
  resolver skips radarAlert rows (no auto-open; barrier-not-a-door intact).

Inputs — one first-class config.inputs[] list (the twin of relays[]):
- Each row is { input, role, relay?, kind?, activeLow?, cooldownSec? } with a
  "+ Add input" button. role ∈ button | presence | alertTrigger; button/presence
  name the relay they serve. An exit radar is just another presence row.
- Keystone `inputsOf(row)`: returns config.inputs[] or SYNTHESIZES it from the
  legacy relays[].button/presenceInput/... fields, so relayForButton /
  relayForPresence resolve identically from either shape — zero-downtime, no
  migration. entry-flow.ts is unchanged (resolves through the same functions).
- Fixed a latent bug this exposed: the alert lamp's camera lock was hardcoded to
  the ENTRY camera. Added relays[].lockLane ("entry"|"exit", default entry); the
  lamp now locks on its own lane's camera, so an exit radar's lamp tracks the exit
  camera. button-light tracks both #entryBusy/#exitBusy.
- Driver: extracted activeLowFrom(config) — merges inputs[] activeLow, legacy
  relays[].presenceActiveLow, and the inputActiveLow escape hatch.

UI: the relay dropdown gained a "Radar alert" option (reveals trigger/lock/blink
inputs); InputEditor is rewritten to a generic list (role select folds loop/radar);
i18n sq+en kept at type-parity.

Tests: new device-resolve.test.ts (inputs[] resolution + legacy fallback identical
+ exit-radar resolves to the exit relay); button-light gains a two-independent-
alert-relays case and an exit-lamp lockLane case; access-dingtian gains
activeLowFrom cases. Full workspace build/lint/test green (i18n parity included).

Wiki + memory updated (button-light-indicator, entry-double-press, dingtian-relay).

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-28 11:23:15 +02:00

63 lines
2.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { activeLowFrom, inputActive } from "./access-dingtian.js";
// Per-input active-level normalisation. The board has ONE resting level, but a radar
// can idle opposite the button — listing its terminal in `activeLow` inverts just that
// input so "present" reads correctly. See wiki/entities/hikvision-radar.md.
describe("inputActive (per-input active-level)", () => {
const none = new Set<number>();
const radarOnI2 = new Set<number>([2]);
it("default board (resting HIGH): a pull LOW is active, HIGH is rest", () => {
// Button on I1, board idles HIGH → active when LOW.
expect(inputActive(false, 1, true, none)).toBe(true); // LOW = pressed
expect(inputActive(true, 1, true, none)).toBe(false); // HIGH = rest
});
it("resting LOW board: a pull HIGH is active", () => {
expect(inputActive(true, 1, false, none)).toBe(true);
expect(inputActive(false, 1, false, none)).toBe(false);
});
it("active-low override inverts ONLY the listed input", () => {
// Board idles HIGH (button on I1), radar on I2 idles HIGH and goes LOW on detect →
// mark I2 active-low so detection (LOW) reads active.
// I1 (button) keeps the board default:
expect(inputActive(false, 1, true, radarOnI2)).toBe(true); // button LOW = active
expect(inputActive(true, 1, true, radarOnI2)).toBe(false);
// I2 (radar) overridden to active-low: active when LOW.
expect(inputActive(false, 2, true, radarOnI2)).toBe(true); // radar LOW = detecting
expect(inputActive(true, 2, true, radarOnI2)).toBe(false); // radar HIGH = clear
});
});
describe("activeLowFrom (config → active-low terminal set)", () => {
it("reads a config.inputs[] presence row with activeLow", () => {
const set = activeLowFrom({
inputs: [
{ input: 1, role: "button", relay: 1 },
{ input: 2, role: "presence", relay: 1, kind: "radar", activeLow: true },
{ input: 5, role: "presence", relay: 2, kind: "radar", activeLow: true }, // exit radar
],
});
expect([...set].sort()).toEqual([2, 5]); // both radars inverted; the button is not
});
it("still reads the LEGACY relays[].presenceActiveLow (back-compat)", () => {
const set = activeLowFrom({
relays: [{ relay: 1, direction: "entry", presenceInput: 2, presenceActiveLow: true }],
});
expect([...set]).toEqual([2]);
});
it("honours an explicit top-level inputActiveLow escape hatch + merges all sources", () => {
const set = activeLowFrom({
inputActiveLow: [3],
inputs: [{ input: 2, role: "presence", activeLow: true }],
relays: [{ relay: 1, direction: "entry", presenceInput: 4, presenceActiveLow: true }],
});
expect([...set].sort()).toEqual([2, 3, 4]);
});
});