HA: side panel frontend (Lit/TypeScript) for the program viewer
Phase C of the program viewer. Replaces the "panel coming soon" stub with a real Lit-based side panel that consumes the Phase-B websocket commands. Layout (top to bottom): * Header — title + total/filtered count * Filter bar — search box (substring match), trigger-type chips (TIMED / EVENT / YEARLY / WHEN / AT / EVERY / REMARK), a clearable "filtering on <ref>" pill when an entity filter is active * Two-column body: program list on the left, slide-in detail panel on the right. Collapses to single-column on `narrow` view. The program list renders one row per program (or per chain head, for clausal multi-record programs). Each row carries the slot number, the rendered one-line summary token stream, and meta pills for trigger type / condition count / multi-action count. The detail panel renders the full structured-English token stream inside a styled <pre>. A "Fire now" button calls ``omni_pca/programs/fire`` over the wire — the panel actually runs the program. For chain detail the spanned slot range is shown underneath. REF tokens are rendered as `<button>` elements that click to filter the list to "programs that mention this entity" — the most useful navigational affordance for the "why is this happening?" use case. Live-state badges (SECURE / NOT READY / ON 60% / Away / 72°F / …) are appended to REF tokens via the Phase-B coordinator-backed StateResolver. The panel polls ``programs/list`` every 5 seconds to refresh badges; switching to push-event subscriptions is a follow-up when polling overhead becomes visible. Theming uses HA's standard CSS variables (--primary-color, --card-background-color, --divider-color, etc.) so the panel inherits the user's HA theme automatically. Build pipeline: * TypeScript source under ``custom_components/omni_pca/frontend/src/`` * esbuild bundles entry → ESM in one self-contained file * Output at ``custom_components/omni_pca/www/panel.js`` (~34 KB minified) is committed so end-users don't need Node installed * ``npm run watch`` for HA-dev-time iteration * tsconfig has strict mode + noUnusedLocals; bundle currently type-checks clean Manifest declares deps on ``http`` and ``websocket_api``; ``frontend`` and ``panel_custom`` are loaded opportunistically (they require ``hass_frontend`` which the test harness doesn't ship — keeping them out of the manifest deps keeps tests green). Full suite: 634 passed, 1 skipped (no test changes; the integration side hasn't moved since Phase B).
This commit is contained in:
parent
821a402d32
commit
f38777e219
5
.gitignore
vendored
5
.gitignore
vendored
@ -42,3 +42,8 @@ panel_key*
|
|||||||
ha-config/
|
ha-config/
|
||||||
dist/
|
dist/
|
||||||
dev/.omni_key
|
dev/.omni_key
|
||||||
|
|
||||||
|
# Frontend build artifacts. node_modules is local; the bundled panel.js
|
||||||
|
# is committed (alongside its source) so end-users don't need Node
|
||||||
|
# installed to use the integration.
|
||||||
|
custom_components/omni_pca/frontend/node_modules/
|
||||||
|
|||||||
41
custom_components/omni_pca/frontend/README.md
Normal file
41
custom_components/omni_pca/frontend/README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Omni Programs side panel — frontend
|
||||||
|
|
||||||
|
Lit/TypeScript source for the HA side panel registered by
|
||||||
|
`websocket.py:async_register_side_panel`. The build output
|
||||||
|
(`../www/panel.js`) is committed so end-users don't need Node installed.
|
||||||
|
|
||||||
|
## Edit / rebuild
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd custom_components/omni_pca/frontend
|
||||||
|
npm install # one-time
|
||||||
|
npm run build # one-shot — drops a fresh ../www/panel.js
|
||||||
|
npm run watch # rebuild on change (use during HA dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
The build script (`build.mjs`) bundles the entry point + Lit + all
|
||||||
|
imports into a single ESM file at `../www/panel.js`. Source maps are
|
||||||
|
inlined in `--watch` mode and stripped in production builds. Output is
|
||||||
|
~34 KB minified.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `src/omni-panel-programs.ts` | The custom-element entry point. Defines `<omni-panel-programs>` (matching the panel_custom registration). |
|
||||||
|
| `src/token-renderer.ts` | Token stream → Lit `TemplateResult`. Each TokenKind gets distinctive styling; REF tokens become buttons that dispatch a click. |
|
||||||
|
| `src/types.ts` | TS interfaces mirroring the Phase-B websocket wire shapes. Short keys (`k`/`t`/`ek`/`ei`/`s`) match `websocket.py:_tokens_to_json`. |
|
||||||
|
|
||||||
|
## Wire contract
|
||||||
|
|
||||||
|
The panel calls three websocket commands (all defined in
|
||||||
|
`../websocket.py`):
|
||||||
|
|
||||||
|
* `omni_pca/programs/list` — paginated, filterable summaries.
|
||||||
|
* `omni_pca/programs/get` — full structured-English detail for one slot.
|
||||||
|
* `omni_pca/programs/fire` — sends `Command.EXECUTE_PROGRAM` over the wire.
|
||||||
|
|
||||||
|
The frontend doesn't subscribe to push events; live-state badges
|
||||||
|
refresh on a low-frequency poll (`REFRESH_MS = 5000`). That's a
|
||||||
|
deliberate scope choice — switching to per-entity event subscription
|
||||||
|
is a follow-up if the polling overhead becomes visible on huge installs.
|
||||||
44
custom_components/omni_pca/frontend/build.mjs
Normal file
44
custom_components/omni_pca/frontend/build.mjs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Bundle the omni_pca side panel into a single ESM file the HA static
|
||||||
|
// path serves at /api/omni_pca/panel.js.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// node build.mjs # one-shot production build
|
||||||
|
// node build.mjs --watch # rebuild on source change
|
||||||
|
//
|
||||||
|
// Output is intentionally placed at ../www/panel.js so the Python side
|
||||||
|
// (websocket.py:async_register_side_panel) finds it without extra
|
||||||
|
// configuration. The frontend dir + the Python integration sit in the
|
||||||
|
// same custom_components/omni_pca/ tree so end-users just install the
|
||||||
|
// integration; no separate HACS package needed.
|
||||||
|
|
||||||
|
import { build, context } from "esbuild";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const watch = process.argv.includes("--watch");
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
entryPoints: [resolve(__dirname, "src/omni-panel-programs.ts")],
|
||||||
|
bundle: true,
|
||||||
|
format: "esm",
|
||||||
|
target: "es2022",
|
||||||
|
minify: !watch,
|
||||||
|
sourcemap: watch ? "inline" : false,
|
||||||
|
outfile: resolve(__dirname, "../www/panel.js"),
|
||||||
|
// Lit ships its own ESM build; bundle it inline so the panel is a
|
||||||
|
// single self-contained file (matches how HACS-distributed cards work).
|
||||||
|
loader: { ".ts": "ts" },
|
||||||
|
banner: {
|
||||||
|
js: "// omni_pca side panel — generated by frontend/build.mjs. Edit src/, not this file.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (watch) {
|
||||||
|
const ctx = await context(opts);
|
||||||
|
await ctx.watch();
|
||||||
|
console.log("watching for changes…");
|
||||||
|
} else {
|
||||||
|
await build(opts);
|
||||||
|
console.log("built ->", opts.outfile);
|
||||||
|
}
|
||||||
551
custom_components/omni_pca/frontend/package-lock.json
generated
Normal file
551
custom_components/omni_pca/frontend/package-lock.json
generated
Normal file
@ -0,0 +1,551 @@
|
|||||||
|
{
|
||||||
|
"name": "omni-pca-panel",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "omni-pca-panel",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lit": "^3.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.24.0",
|
||||||
|
"typescript": "^5.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@lit/reactive-element": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@lit-labs/ssr-dom-shim": "^1.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.24.2",
|
||||||
|
"@esbuild/android-arm": "0.24.2",
|
||||||
|
"@esbuild/android-arm64": "0.24.2",
|
||||||
|
"@esbuild/android-x64": "0.24.2",
|
||||||
|
"@esbuild/darwin-arm64": "0.24.2",
|
||||||
|
"@esbuild/darwin-x64": "0.24.2",
|
||||||
|
"@esbuild/freebsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/freebsd-x64": "0.24.2",
|
||||||
|
"@esbuild/linux-arm": "0.24.2",
|
||||||
|
"@esbuild/linux-arm64": "0.24.2",
|
||||||
|
"@esbuild/linux-ia32": "0.24.2",
|
||||||
|
"@esbuild/linux-loong64": "0.24.2",
|
||||||
|
"@esbuild/linux-mips64el": "0.24.2",
|
||||||
|
"@esbuild/linux-ppc64": "0.24.2",
|
||||||
|
"@esbuild/linux-riscv64": "0.24.2",
|
||||||
|
"@esbuild/linux-s390x": "0.24.2",
|
||||||
|
"@esbuild/linux-x64": "0.24.2",
|
||||||
|
"@esbuild/netbsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/netbsd-x64": "0.24.2",
|
||||||
|
"@esbuild/openbsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/openbsd-x64": "0.24.2",
|
||||||
|
"@esbuild/sunos-x64": "0.24.2",
|
||||||
|
"@esbuild/win32-arm64": "0.24.2",
|
||||||
|
"@esbuild/win32-ia32": "0.24.2",
|
||||||
|
"@esbuild/win32-x64": "0.24.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lit": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@lit/reactive-element": "^2.1.0",
|
||||||
|
"lit-element": "^4.2.0",
|
||||||
|
"lit-html": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lit-element": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@lit-labs/ssr-dom-shim": "^1.5.0",
|
||||||
|
"@lit/reactive-element": "^2.1.0",
|
||||||
|
"lit-html": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lit-html": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
custom_components/omni_pca/frontend/package.json
Normal file
18
custom_components/omni_pca/frontend/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "omni-pca-panel",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "HA side panel for browsing HAI Omni Panel programs.",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node build.mjs",
|
||||||
|
"watch": "node build.mjs --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.24.0",
|
||||||
|
"typescript": "^5.5.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lit": "^3.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
613
custom_components/omni_pca/frontend/src/omni-panel-programs.ts
Normal file
613
custom_components/omni_pca/frontend/src/omni-panel-programs.ts
Normal file
@ -0,0 +1,613 @@
|
|||||||
|
// Side panel for browsing HAI Omni Panel programs. Custom-element name
|
||||||
|
// matches the websocket.py:_PANEL_WEBCOMPONENT registration.
|
||||||
|
//
|
||||||
|
// Layout: filter chips + search across the top, paginated program list
|
||||||
|
// in the main column, sliding detail panel on the right when a row is
|
||||||
|
// selected. Live-state badges refresh on a low-frequency poll
|
||||||
|
// (`state_changed` events would be more efficient but require
|
||||||
|
// per-entity subscription bookkeeping; the poll keeps Phase C scope
|
||||||
|
// honest — easy upgrade target later).
|
||||||
|
|
||||||
|
import { LitElement, html, css, PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
import { renderTokens } from "./token-renderer.js";
|
||||||
|
import {
|
||||||
|
Hass,
|
||||||
|
ProgramDetail,
|
||||||
|
ProgramListResponse,
|
||||||
|
ProgramRow,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
const TRIGGER_TYPES = [
|
||||||
|
"TIMED", "EVENT", "YEARLY", "WHEN", "AT", "EVERY", "REMARK",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// How often the live-state overlay refreshes. Low enough that a panel
|
||||||
|
// fire shows up promptly, high enough that 330 programs × 4-byte deltas
|
||||||
|
// don't generate websocket noise.
|
||||||
|
const REFRESH_MS = 5000;
|
||||||
|
|
||||||
|
|
||||||
|
@customElement("omni-panel-programs")
|
||||||
|
export class OmniPanelPrograms extends LitElement {
|
||||||
|
// -- HA-supplied properties -------------------------------------------
|
||||||
|
|
||||||
|
@property({ attribute: false }) hass!: Hass;
|
||||||
|
@property({ attribute: false }) narrow = false;
|
||||||
|
|
||||||
|
// -- local state ------------------------------------------------------
|
||||||
|
|
||||||
|
/** All omni_pca config entries discovered from hass.config.entries.
|
||||||
|
* Currently we just pick the first one — multi-panel installs would
|
||||||
|
* add a selector here. The websocket commands take an explicit
|
||||||
|
* entry_id so this is a UI concern, not a wire concern. */
|
||||||
|
@state() private _entryId: string | null = null;
|
||||||
|
|
||||||
|
@state() private _rows: ProgramRow[] = [];
|
||||||
|
@state() private _total = 0;
|
||||||
|
@state() private _filteredTotal = 0;
|
||||||
|
@state() private _loading = false;
|
||||||
|
@state() private _error: string | null = null;
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
@state() private _activeTriggerTypes: Set<string> = new Set();
|
||||||
|
@state() private _referenceFilter: string | null = null;
|
||||||
|
@state() private _searchTerm: string = "";
|
||||||
|
|
||||||
|
// Detail panel
|
||||||
|
@state() private _selectedSlot: number | null = null;
|
||||||
|
@state() private _detail: ProgramDetail | null = null;
|
||||||
|
@state() private _detailLoading = false;
|
||||||
|
@state() private _fireFeedback: string | null = null;
|
||||||
|
|
||||||
|
private _refreshTimer: number | null = null;
|
||||||
|
|
||||||
|
// -- lifecycle --------------------------------------------------------
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._discoverEntry();
|
||||||
|
if (this._entryId) {
|
||||||
|
this._loadList();
|
||||||
|
this._startRefreshTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._stopRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed: PropertyValues): void {
|
||||||
|
if (changed.has("hass") && this._entryId === null) {
|
||||||
|
this._discoverEntry();
|
||||||
|
if (this._entryId) {
|
||||||
|
this._loadList();
|
||||||
|
this._startRefreshTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- entry discovery --------------------------------------------------
|
||||||
|
|
||||||
|
private _discoverEntry(): void {
|
||||||
|
// hass.config.entries shape varies by HA version; most installs have
|
||||||
|
// exactly one omni_pca panel, so we just probe a list endpoint to
|
||||||
|
// find any entry_id our websocket accepts.
|
||||||
|
if (!this.hass?.connection) return;
|
||||||
|
void this._discoverViaList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _discoverViaList(): Promise<void> {
|
||||||
|
// Best-effort: walk known config entries via HA's standard
|
||||||
|
// config_entries/get command. If that fails we surface a friendly
|
||||||
|
// error pointing at integration setup.
|
||||||
|
try {
|
||||||
|
const entries = await this.hass.connection.sendMessagePromise<
|
||||||
|
Array<{ entry_id: string; domain: string; title: string }>
|
||||||
|
>({ type: "config_entries/get" });
|
||||||
|
const ours = entries.filter((e) => e.domain === "omni_pca");
|
||||||
|
if (ours.length === 0) {
|
||||||
|
this._error = "No Omni panel configured. Add one via Settings → Devices & Services.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// First entry wins for v1; multi-panel selector is a follow-up.
|
||||||
|
this._entryId = ours[0].entry_id;
|
||||||
|
this._error = null;
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Could not discover panels: ${err instanceof Error ? err.message : String(err)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- data loading -----------------------------------------------------
|
||||||
|
|
||||||
|
private async _loadList(): Promise<void> {
|
||||||
|
if (!this._entryId) return;
|
||||||
|
this._loading = true;
|
||||||
|
this._error = null;
|
||||||
|
try {
|
||||||
|
const msg: Record<string, unknown> = {
|
||||||
|
type: "omni_pca/programs/list",
|
||||||
|
entry_id: this._entryId,
|
||||||
|
};
|
||||||
|
if (this._activeTriggerTypes.size > 0) {
|
||||||
|
msg.trigger_types = [...this._activeTriggerTypes];
|
||||||
|
}
|
||||||
|
if (this._referenceFilter) {
|
||||||
|
msg.references_entity = this._referenceFilter;
|
||||||
|
}
|
||||||
|
if (this._searchTerm) {
|
||||||
|
msg.search = this._searchTerm;
|
||||||
|
}
|
||||||
|
const result = await this.hass.connection.sendMessagePromise<ProgramListResponse>(msg);
|
||||||
|
this._rows = result.programs;
|
||||||
|
this._total = result.total;
|
||||||
|
this._filteredTotal = result.filtered_total;
|
||||||
|
} catch (err) {
|
||||||
|
this._error = err instanceof Error ? err.message : String(err);
|
||||||
|
} finally {
|
||||||
|
this._loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadDetail(slot: number): Promise<void> {
|
||||||
|
if (!this._entryId) return;
|
||||||
|
this._detailLoading = true;
|
||||||
|
this._detail = null;
|
||||||
|
try {
|
||||||
|
this._detail = await this.hass.connection.sendMessagePromise({
|
||||||
|
type: "omni_pca/programs/get",
|
||||||
|
entry_id: this._entryId,
|
||||||
|
slot,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this._error = err instanceof Error ? err.message : String(err);
|
||||||
|
} finally {
|
||||||
|
this._detailLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fireProgram(slot: number): Promise<void> {
|
||||||
|
if (!this._entryId) return;
|
||||||
|
this._fireFeedback = "firing…";
|
||||||
|
try {
|
||||||
|
await this.hass.connection.sendMessagePromise({
|
||||||
|
type: "omni_pca/programs/fire",
|
||||||
|
entry_id: this._entryId,
|
||||||
|
slot,
|
||||||
|
});
|
||||||
|
this._fireFeedback = `fired slot ${slot}`;
|
||||||
|
// Live-state will refresh on the next poll tick.
|
||||||
|
} catch (err) {
|
||||||
|
this._fireFeedback = `error: ${err instanceof Error ? err.message : err}`;
|
||||||
|
}
|
||||||
|
// Auto-clear feedback after a beat.
|
||||||
|
setTimeout(() => { this._fireFeedback = null; }, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- refresh timer ----------------------------------------------------
|
||||||
|
|
||||||
|
private _startRefreshTimer(): void {
|
||||||
|
if (this._refreshTimer !== null) return;
|
||||||
|
this._refreshTimer = window.setInterval(() => {
|
||||||
|
void this._loadList();
|
||||||
|
if (this._selectedSlot !== null) {
|
||||||
|
void this._loadDetail(this._selectedSlot);
|
||||||
|
}
|
||||||
|
}, REFRESH_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopRefreshTimer(): void {
|
||||||
|
if (this._refreshTimer !== null) {
|
||||||
|
window.clearInterval(this._refreshTimer);
|
||||||
|
this._refreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- filter handlers --------------------------------------------------
|
||||||
|
|
||||||
|
private _toggleTriggerFilter(t: string): void {
|
||||||
|
const next = new Set(this._activeTriggerTypes);
|
||||||
|
if (next.has(t)) next.delete(t); else next.add(t);
|
||||||
|
this._activeTriggerTypes = next;
|
||||||
|
void this._loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onSearchInput(e: Event): void {
|
||||||
|
this._searchTerm = (e.target as HTMLInputElement).value;
|
||||||
|
void this._loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearReferenceFilter(): void {
|
||||||
|
this._referenceFilter = null;
|
||||||
|
void this._loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onRowClick(slot: number): void {
|
||||||
|
this._selectedSlot = slot;
|
||||||
|
void this._loadDetail(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onRefClick(kind: string, id: number): void {
|
||||||
|
// Click on any object reference in a token stream → filter the
|
||||||
|
// list to programs that mention that entity. Clears the detail
|
||||||
|
// panel since the new filter scope makes the old selection less
|
||||||
|
// meaningful.
|
||||||
|
this._referenceFilter = `${kind}:${id}`;
|
||||||
|
this._selectedSlot = null;
|
||||||
|
this._detail = null;
|
||||||
|
void this._loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _closeDetail(): void {
|
||||||
|
this._selectedSlot = null;
|
||||||
|
this._detail = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- render -----------------------------------------------------------
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
<ha-icon icon="mdi:script-text-outline"></ha-icon>
|
||||||
|
<span>Omni Programs</span>
|
||||||
|
${this._total > 0 ? html`
|
||||||
|
<span class="count">
|
||||||
|
${this._filteredTotal === this._total
|
||||||
|
? `${this._total} programs`
|
||||||
|
: `${this._filteredTotal} of ${this._total} shown`}
|
||||||
|
</span>` : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this._error ? html`
|
||||||
|
<div class="error">${this._error}</div>` : ""}
|
||||||
|
${this._renderFilters()}
|
||||||
|
<div class="body" data-narrow=${this.narrow}>
|
||||||
|
${this._renderList()}
|
||||||
|
${this._selectedSlot !== null ? this._renderDetail() : ""}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderFilters(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="filters">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
class="search"
|
||||||
|
placeholder="search programs..."
|
||||||
|
.value=${this._searchTerm}
|
||||||
|
@input=${this._onSearchInput}
|
||||||
|
/>
|
||||||
|
<div class="chips">
|
||||||
|
${TRIGGER_TYPES.map((t) => html`
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chip ${this._activeTriggerTypes.has(t) ? "active" : ""}"
|
||||||
|
@click=${() => this._toggleTriggerFilter(t)}
|
||||||
|
>${t}</button>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
${this._referenceFilter ? html`
|
||||||
|
<div class="ref-filter">
|
||||||
|
<span>filtering on <strong>${this._referenceFilter}</strong></span>
|
||||||
|
<button type="button" @click=${this._clearReferenceFilter}>clear</button>
|
||||||
|
</div>` : ""}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderList(): TemplateResult {
|
||||||
|
if (this._loading && this._rows.length === 0) {
|
||||||
|
return html`<div class="loading">loading…</div>`;
|
||||||
|
}
|
||||||
|
if (this._rows.length === 0) {
|
||||||
|
return html`<div class="empty">No programs match the current filters.</div>`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="list">
|
||||||
|
${this._rows.map((row) => html`
|
||||||
|
<div
|
||||||
|
class="row ${this._selectedSlot === row.slot ? "selected" : ""}"
|
||||||
|
@click=${() => this._onRowClick(row.slot)}
|
||||||
|
>
|
||||||
|
<div class="row-slot">#${row.slot}</div>
|
||||||
|
<div class="row-summary">
|
||||||
|
${renderTokens(row.summary, (k, i) => this._onRefClick(k, i))}
|
||||||
|
</div>
|
||||||
|
<div class="row-meta">
|
||||||
|
<span class="trigger-badge trigger-${row.trigger_type.toLowerCase()}">
|
||||||
|
${row.trigger_type}
|
||||||
|
</span>
|
||||||
|
${row.condition_count > 0 ? html`
|
||||||
|
<span class="meta-pill">${row.condition_count} cond</span>` : ""}
|
||||||
|
${row.action_count > 1 ? html`
|
||||||
|
<span class="meta-pill">${row.action_count} actions</span>` : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderDetail(): TemplateResult {
|
||||||
|
if (this._detailLoading) {
|
||||||
|
return html`<aside class="detail"><div class="loading">loading…</div></aside>`;
|
||||||
|
}
|
||||||
|
if (this._detail === null) {
|
||||||
|
return html`<aside class="detail"></aside>`;
|
||||||
|
}
|
||||||
|
const d = this._detail;
|
||||||
|
return html`
|
||||||
|
<aside class="detail">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<span class="trigger-badge trigger-${d.trigger_type.toLowerCase()}">
|
||||||
|
${d.trigger_type}
|
||||||
|
</span>
|
||||||
|
<span class="slot">slot #${d.slot}</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="close" @click=${this._closeDetail}>×</button>
|
||||||
|
</header>
|
||||||
|
<pre class="detail-body">${renderTokens(d.tokens, (k, i) => this._onRefClick(k, i))}</pre>
|
||||||
|
<footer>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="fire"
|
||||||
|
@click=${() => this._fireProgram(d.slot)}
|
||||||
|
>▶ Fire now</button>
|
||||||
|
${this._fireFeedback ? html`
|
||||||
|
<span class="fire-feedback">${this._fireFeedback}</span>` : ""}
|
||||||
|
</footer>
|
||||||
|
${d.chain_slots && d.chain_slots.length > 1 ? html`
|
||||||
|
<div class="chain-info">
|
||||||
|
spans slots
|
||||||
|
${d.chain_slots.map((s, i) => html`
|
||||||
|
${i > 0 ? "→" : ""}#${s}`)}
|
||||||
|
</div>` : ""}
|
||||||
|
</aside>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- styles -----------------------------------------------------------
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--primary-background-color, #fafafa);
|
||||||
|
color: var(--primary-text-color, #000);
|
||||||
|
font-family: var(--paper-font-body1_-_font-family, sans-serif);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex; align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: var(--primary-color, #03a9f4);
|
||||||
|
color: var(--text-primary-color, #fff);
|
||||||
|
}
|
||||||
|
.header .title { display: flex; align-items: center; gap: 10px; font-size: 1.2rem; }
|
||||||
|
.header .count {
|
||||||
|
margin-left: 12px;
|
||||||
|
font-size: 0.85rem; opacity: 0.85; font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin: 12px 16px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: var(--error-color, #db4437);
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
padding: 12px 16px 8px;
|
||||||
|
border-bottom: 1px solid var(--divider-color, #ddd);
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border: 1px solid var(--divider-color, #ccc);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--card-background-color, #fff);
|
||||||
|
color: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.chips {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
border: 1px solid var(--divider-color, #ccc);
|
||||||
|
background: var(--card-background-color, #fff);
|
||||||
|
color: var(--secondary-text-color, #555);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.chip:hover { background: var(--secondary-background-color, #eee); }
|
||||||
|
.chip.active {
|
||||||
|
background: var(--primary-color, #03a9f4);
|
||||||
|
color: var(--text-primary-color, #fff);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.ref-filter {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--secondary-text-color, #555);
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
}
|
||||||
|
.ref-filter button {
|
||||||
|
border: 1px solid var(--divider-color, #ccc);
|
||||||
|
background: transparent; color: inherit;
|
||||||
|
padding: 2px 8px; border-radius: 8px;
|
||||||
|
font-size: 0.75rem; cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.body[data-narrow="false"] { grid-template-columns: 1fr 380px; }
|
||||||
|
|
||||||
|
.list {
|
||||||
|
max-height: calc(100vh - 200px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 60px 1fr auto;
|
||||||
|
align-items: start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--divider-color, #eee);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.row:hover { background: var(--secondary-background-color, #f5f5f5); }
|
||||||
|
.row.selected { background: var(--state-active-color, #e3f2fd); }
|
||||||
|
.row-slot {
|
||||||
|
font-family: var(--code-font-family, monospace);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--secondary-text-color, #888);
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
.row-summary {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.row-meta {
|
||||||
|
display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* trigger-type badges */
|
||||||
|
.trigger-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.trigger-timed { background: #e3f2fd; color: #1565c0; }
|
||||||
|
.trigger-event { background: #fff3e0; color: #e65100; }
|
||||||
|
.trigger-yearly { background: #f3e5f5; color: #6a1b9a; }
|
||||||
|
.trigger-when { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.trigger-at { background: #e3f2fd; color: #1565c0; }
|
||||||
|
.trigger-every { background: #fce4ec; color: #ad1457; }
|
||||||
|
.trigger-remark { background: #f5f5f5; color: #616161; }
|
||||||
|
|
||||||
|
.meta-pill {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--secondary-text-color, #888);
|
||||||
|
background: var(--secondary-background-color, #eee);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* token-renderer styles */
|
||||||
|
.row-summary, .detail-body {
|
||||||
|
font-family: var(--paper-font-body1_-_font-family, system-ui, sans-serif);
|
||||||
|
}
|
||||||
|
.keyword { font-weight: 600; color: var(--primary-color, #1565c0); }
|
||||||
|
.operator { color: var(--secondary-text-color, #666); font-style: italic; }
|
||||||
|
.value { font-family: var(--code-font-family, monospace); color: var(--accent-color, #ff6f00); }
|
||||||
|
.ref {
|
||||||
|
display: inline-flex; align-items: baseline; gap: 4px;
|
||||||
|
border: none; background: transparent; padding: 0 2px;
|
||||||
|
cursor: pointer; font: inherit; color: inherit;
|
||||||
|
border-bottom: 1px dotted var(--secondary-text-color, #999);
|
||||||
|
}
|
||||||
|
.ref:hover { background: var(--secondary-background-color, #eee); }
|
||||||
|
.ref-name { font-weight: 500; }
|
||||||
|
.ref-state {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--secondary-background-color, #eee);
|
||||||
|
color: var(--secondary-text-color, #666);
|
||||||
|
vertical-align: 1px;
|
||||||
|
}
|
||||||
|
.ref-zone .ref-name { color: var(--info-color, #0288d1); }
|
||||||
|
.ref-unit .ref-name { color: var(--warning-color, #f57c00); }
|
||||||
|
.ref-area .ref-name { color: var(--success-color, #388e3c); }
|
||||||
|
.ref-thermostat .ref-name { color: var(--accent-color, #c2185b); }
|
||||||
|
.ref-button .ref-name { color: var(--state-light-color, #7e57c2); }
|
||||||
|
|
||||||
|
.indent { display: inline-block; width: 1.5em; }
|
||||||
|
|
||||||
|
/* detail panel */
|
||||||
|
.detail {
|
||||||
|
border-left: 1px solid var(--divider-color, #ddd);
|
||||||
|
padding: 16px;
|
||||||
|
max-height: calc(100vh - 200px);
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.body[data-narrow="true"] .detail {
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid var(--divider-color, #ddd);
|
||||||
|
}
|
||||||
|
.detail header {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.detail header .slot {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-family: var(--code-font-family, monospace);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--secondary-text-color, #888);
|
||||||
|
}
|
||||||
|
.detail .close {
|
||||||
|
background: transparent; border: none;
|
||||||
|
font-size: 1.4rem; cursor: pointer;
|
||||||
|
color: var(--secondary-text-color, #888);
|
||||||
|
}
|
||||||
|
.detail-body {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
background: var(--card-background-color, #fff);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--divider-color, #eee);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.detail footer {
|
||||||
|
display: flex; align-items: center; gap: 12px; margin-top: 14px;
|
||||||
|
}
|
||||||
|
.fire {
|
||||||
|
background: var(--primary-color, #03a9f4);
|
||||||
|
color: var(--text-primary-color, #fff);
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.fire:hover { filter: brightness(0.9); }
|
||||||
|
.fire-feedback {
|
||||||
|
font-size: 0.85rem; color: var(--secondary-text-color, #666);
|
||||||
|
}
|
||||||
|
.chain-info {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--secondary-text-color, #888);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading, .empty {
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--secondary-text-color, #888);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"omni-panel-programs": OmniPanelPrograms;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
custom_components/omni_pca/frontend/src/token-renderer.ts
Normal file
52
custom_components/omni_pca/frontend/src/token-renderer.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// Token-stream → DOM. Each TokenKind gets distinctive styling so the
|
||||||
|
// structured-English programs read cleanly even at a glance.
|
||||||
|
//
|
||||||
|
// REF tokens are rendered as <button> nodes so they can dispatch a
|
||||||
|
// "ref-click" event for the parent component to act on (filter the
|
||||||
|
// list, jump to that entity's HA page, etc.).
|
||||||
|
|
||||||
|
import { html, TemplateResult } from "lit";
|
||||||
|
import { Token } from "./types.js";
|
||||||
|
|
||||||
|
export function renderTokens(
|
||||||
|
tokens: Token[],
|
||||||
|
onRefClick?: (kind: string, id: number) => void,
|
||||||
|
): TemplateResult {
|
||||||
|
return html`${tokens.map((t) => renderToken(t, onRefClick))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToken(
|
||||||
|
t: Token,
|
||||||
|
onRefClick?: (kind: string, id: number) => void,
|
||||||
|
): TemplateResult {
|
||||||
|
switch (t.k) {
|
||||||
|
case "newline":
|
||||||
|
return html`<br />`;
|
||||||
|
case "indent":
|
||||||
|
// Convert leading spaces to a CSS class so the panel can switch
|
||||||
|
// indent styling (e.g. left border) without re-rendering tokens.
|
||||||
|
return html`<span class="indent">${t.t}</span>`;
|
||||||
|
case "keyword":
|
||||||
|
return html`<span class="keyword">${t.t}</span>`;
|
||||||
|
case "operator":
|
||||||
|
return html`<span class="operator">${t.t}</span>`;
|
||||||
|
case "value":
|
||||||
|
return html`<span class="value">${t.t}</span>`;
|
||||||
|
case "ref": {
|
||||||
|
const handler = onRefClick && t.ek && typeof t.ei === "number"
|
||||||
|
? () => onRefClick(t.ek!, t.ei!)
|
||||||
|
: undefined;
|
||||||
|
return html`<button
|
||||||
|
type="button"
|
||||||
|
class="ref ref-${t.ek}"
|
||||||
|
title=${t.ek ?? ""}
|
||||||
|
@click=${handler}
|
||||||
|
>
|
||||||
|
<span class="ref-name">${t.t}</span>
|
||||||
|
${t.s ? html`<span class="ref-state">${t.s}</span>` : ""}
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return html`<span>${t.t}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
custom_components/omni_pca/frontend/src/types.ts
Normal file
87
custom_components/omni_pca/frontend/src/types.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// TS mirrors of the Phase-B websocket wire shapes. Short field names
|
||||||
|
// match websocket.py's _tokens_to_json — keep these in sync if the
|
||||||
|
// Python side changes.
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
/** "keyword" / "operator" / "ref" / "value" / "text" / "indent" / "newline" */
|
||||||
|
k: string;
|
||||||
|
/** Display text for this token. Empty for newline. */
|
||||||
|
t: string;
|
||||||
|
/** Object kind for REF tokens (zone / unit / area / thermostat / button / message / code / timeclock). */
|
||||||
|
ek?: string;
|
||||||
|
/** 1-based slot for REF tokens. */
|
||||||
|
ei?: number;
|
||||||
|
/** Live-state badge for REF tokens (e.g. "SECURE", "ON 60%"). */
|
||||||
|
s?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgramRow {
|
||||||
|
/** 1-based slot number. For chains, the head slot. */
|
||||||
|
slot: number;
|
||||||
|
/** "compact" or "chain". */
|
||||||
|
kind: string;
|
||||||
|
/** TIMED / EVENT / YEARLY / WHEN / AT / EVERY / REMARK / FREE. */
|
||||||
|
trigger_type: string;
|
||||||
|
/** One-line summary token stream. */
|
||||||
|
summary: Token[];
|
||||||
|
/** Flat ["unit:7", "zone:5", ...] for filter chips. */
|
||||||
|
references: string[];
|
||||||
|
condition_count: number;
|
||||||
|
action_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgramListResponse {
|
||||||
|
programs: ProgramRow[];
|
||||||
|
total: number;
|
||||||
|
filtered_total: number;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgramDetail {
|
||||||
|
slot: number;
|
||||||
|
kind: string;
|
||||||
|
trigger_type: string;
|
||||||
|
/** Full structured-English token stream. */
|
||||||
|
tokens: Token[];
|
||||||
|
references: string[];
|
||||||
|
/** For chain detail: every slot the chain spans. */
|
||||||
|
chain_slots?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgramListRequest {
|
||||||
|
type: "omni_pca/programs/list";
|
||||||
|
entry_id: string;
|
||||||
|
trigger_types?: string[];
|
||||||
|
references_entity?: string;
|
||||||
|
search?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgramGetRequest {
|
||||||
|
type: "omni_pca/programs/get";
|
||||||
|
entry_id: string;
|
||||||
|
slot: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgramFireRequest {
|
||||||
|
type: "omni_pca/programs/fire";
|
||||||
|
entry_id: string;
|
||||||
|
slot: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** HA's hass object — minimal surface we use. */
|
||||||
|
export interface Hass {
|
||||||
|
connection: {
|
||||||
|
sendMessagePromise<T>(msg: unknown): Promise<T>;
|
||||||
|
subscribeEvents<T>(
|
||||||
|
callback: (event: T) => void,
|
||||||
|
eventType: string,
|
||||||
|
): Promise<() => Promise<void>>;
|
||||||
|
};
|
||||||
|
config?: {
|
||||||
|
entries?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
// Whole hass is much larger; we only touch what we need.
|
||||||
|
}
|
||||||
20
custom_components/omni_pca/frontend/tsconfig.json
Normal file
20
custom_components/omni_pca/frontend/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"allowImportingTsExtensions": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
"codeowners": ["@rsp2k"],
|
"codeowners": ["@rsp2k"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["http", "websocket_api"],
|
"dependencies": ["http", "websocket_api"],
|
||||||
"documentation": "https://github.com/rsp2k/omni-pca",
|
"documentation": "https://hai-omni-pro-ii.warehack.ing/",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"issue_tracker": "https://github.com/rsp2k/omni-pca/issues",
|
"issue_tracker": "https://github.com/rsp2k/omni-pca/issues",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
2
uv.lock
generated
2
uv.lock
generated
@ -1511,7 +1511,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "omni-pca"
|
name = "omni-pca"
|
||||||
version = "2026.5.11"
|
version = "2026.5.14"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user