diff --git a/.gitignore b/.gitignore index da978ae..34adc0d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,8 @@ panel_key* ha-config/ dist/ 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/ diff --git a/custom_components/omni_pca/frontend/README.md b/custom_components/omni_pca/frontend/README.md new file mode 100644 index 0000000..9ef1f37 --- /dev/null +++ b/custom_components/omni_pca/frontend/README.md @@ -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 `` (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. diff --git a/custom_components/omni_pca/frontend/build.mjs b/custom_components/omni_pca/frontend/build.mjs new file mode 100644 index 0000000..63b0943 --- /dev/null +++ b/custom_components/omni_pca/frontend/build.mjs @@ -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); +} diff --git a/custom_components/omni_pca/frontend/package-lock.json b/custom_components/omni_pca/frontend/package-lock.json new file mode 100644 index 0000000..14b5366 --- /dev/null +++ b/custom_components/omni_pca/frontend/package-lock.json @@ -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" + } + } + } +} diff --git a/custom_components/omni_pca/frontend/package.json b/custom_components/omni_pca/frontend/package.json new file mode 100644 index 0000000..0a422b5 --- /dev/null +++ b/custom_components/omni_pca/frontend/package.json @@ -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" + } +} diff --git a/custom_components/omni_pca/frontend/src/omni-panel-programs.ts b/custom_components/omni_pca/frontend/src/omni-panel-programs.ts new file mode 100644 index 0000000..c5250b8 --- /dev/null +++ b/custom_components/omni_pca/frontend/src/omni-panel-programs.ts @@ -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 = 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 { + // 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 { + if (!this._entryId) return; + this._loading = true; + this._error = null; + try { + const msg: Record = { + 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(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 { + 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 { + 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` +
+
+ + Omni Programs + ${this._total > 0 ? html` + + ${this._filteredTotal === this._total + ? `${this._total} programs` + : `${this._filteredTotal} of ${this._total} shown`} + ` : ""} +
+
+ ${this._error ? html` +
${this._error}
` : ""} + ${this._renderFilters()} +
+ ${this._renderList()} + ${this._selectedSlot !== null ? this._renderDetail() : ""} +
+ `; + } + + private _renderFilters(): TemplateResult { + return html` +
+ +
+ ${TRIGGER_TYPES.map((t) => html` + + `)} +
+ ${this._referenceFilter ? html` +
+ filtering on ${this._referenceFilter} + +
` : ""} +
+ `; + } + + private _renderList(): TemplateResult { + if (this._loading && this._rows.length === 0) { + return html`
loading…
`; + } + if (this._rows.length === 0) { + return html`
No programs match the current filters.
`; + } + return html` +
+ ${this._rows.map((row) => html` +
this._onRowClick(row.slot)} + > +
#${row.slot}
+
+ ${renderTokens(row.summary, (k, i) => this._onRefClick(k, i))} +
+
+ + ${row.trigger_type} + + ${row.condition_count > 0 ? html` + ${row.condition_count} cond` : ""} + ${row.action_count > 1 ? html` + ${row.action_count} actions` : ""} +
+
+ `)} +
+ `; + } + + private _renderDetail(): TemplateResult { + if (this._detailLoading) { + return html``; + } + if (this._detail === null) { + return html``; + } + const d = this._detail; + return html` + + `; + } + + // -- 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; + } +} diff --git a/custom_components/omni_pca/frontend/src/token-renderer.ts b/custom_components/omni_pca/frontend/src/token-renderer.ts new file mode 100644 index 0000000..0765a39 --- /dev/null +++ b/custom_components/omni_pca/frontend/src/token-renderer.ts @@ -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 `; + } + default: + return html`${t.t}`; + } +} diff --git a/custom_components/omni_pca/frontend/src/types.ts b/custom_components/omni_pca/frontend/src/types.ts new file mode 100644 index 0000000..005bda4 --- /dev/null +++ b/custom_components/omni_pca/frontend/src/types.ts @@ -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(msg: unknown): Promise; + subscribeEvents( + callback: (event: T) => void, + eventType: string, + ): Promise<() => Promise>; + }; + config?: { + entries?: Record; + }; + // Whole hass is much larger; we only touch what we need. +} diff --git a/custom_components/omni_pca/frontend/tsconfig.json b/custom_components/omni_pca/frontend/tsconfig.json new file mode 100644 index 0000000..bf351aa --- /dev/null +++ b/custom_components/omni_pca/frontend/tsconfig.json @@ -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"] +} diff --git a/custom_components/omni_pca/manifest.json b/custom_components/omni_pca/manifest.json index e7e19e5..bb4dd83 100644 --- a/custom_components/omni_pca/manifest.json +++ b/custom_components/omni_pca/manifest.json @@ -4,7 +4,7 @@ "codeowners": ["@rsp2k"], "config_flow": true, "dependencies": ["http", "websocket_api"], - "documentation": "https://github.com/rsp2k/omni-pca", + "documentation": "https://hai-omni-pro-ii.warehack.ing/", "integration_type": "hub", "iot_class": "local_push", "issue_tracker": "https://github.com/rsp2k/omni-pca/issues", diff --git a/custom_components/omni_pca/www/panel.js b/custom_components/omni_pca/www/panel.js index 53c5e76..2d55cd3 100644 --- a/custom_components/omni_pca/www/panel.js +++ b/custom_components/omni_pca/www/panel.js @@ -1,20 +1,443 @@ -// omni-pca side panel — stub until Phase C frontend lands. -class OmniPanelPrograms extends HTMLElement { - set hass(hass) { - if (!this._rendered) { - this.innerHTML = ` - -
-

Omni Programs

-

Frontend bundle not yet installed. - Phase C of the program viewer will populate this panel.

-
`; - this._rendered = true; +// omni_pca side panel — generated by frontend/build.mjs. Edit src/, not this file. +var xe=Object.defineProperty;var Ae=Object.getOwnPropertyDescriptor;var f=(i,e,t,r)=>{for(var s=r>1?void 0:r?Ae(e,t):e,o=i.length-1,n;o>=0;o--)(n=i[o])&&(s=(r?n(e,t,s):n(s))||s);return r&&s&&xe(e,t,s),s};var D=globalThis,I=D.ShadowRoot&&(D.ShadyCSS===void 0||D.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,V=Symbol(),ie=new WeakMap,T=class{constructor(e,t,r){if(this._$cssResult$=!0,r!==V)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o,t=this.t;if(I&&e===void 0){let r=t!==void 0&&t.length===1;r&&(e=ie.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&ie.set(t,e))}return e}toString(){return this.cssText}},oe=i=>new T(typeof i=="string"?i:i+"",void 0,V),B=(i,...e)=>{let t=i.length===1?i[0]:e.reduce((r,s,o)=>r+(n=>{if(n._$cssResult$===!0)return n.cssText;if(typeof n=="number")return n;throw Error("Value passed to 'css' function must be a 'css' function result: "+n+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+i[o+1],i[0]);return new T(t,i,V)},ne=(i,e)=>{if(I)i.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let r=document.createElement("style"),s=D.litNonce;s!==void 0&&r.setAttribute("nonce",s),r.textContent=t.cssText,i.appendChild(r)}},W=I?i=>i:i=>i instanceof CSSStyleSheet?(e=>{let t="";for(let r of e.cssRules)t+=r.cssText;return oe(t)})(i):i;var{is:Se,defineProperty:Ee,getOwnPropertyDescriptor:we,getOwnPropertyNames:ke,getOwnPropertySymbols:Te,getPrototypeOf:Ce}=Object,O=globalThis,ae=O.trustedTypes,Re=ae?ae.emptyScript:"",Me=O.reactiveElementPolyfillSupport,C=(i,e)=>i,R={toAttribute(i,e){switch(e){case Boolean:i=i?Re:null;break;case Object:case Array:i=i==null?i:JSON.stringify(i)}return i},fromAttribute(i,e){let t=i;switch(e){case Boolean:t=i!==null;break;case Number:t=i===null?null:Number(i);break;case Object:case Array:try{t=JSON.parse(i)}catch{t=null}}return t}},F=(i,e)=>!Se(i,e),le={attribute:!0,type:String,converter:R,reflect:!1,useDefault:!1,hasChanged:F};Symbol.metadata??=Symbol("metadata"),O.litPropertyMetadata??=new WeakMap;var v=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=le){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){let r=Symbol(),s=this.getPropertyDescriptor(e,r,t);s!==void 0&&Ee(this.prototype,e,s)}}static getPropertyDescriptor(e,t,r){let{get:s,set:o}=we(this.prototype,e)??{get(){return this[t]},set(n){this[t]=n}};return{get:s,set(n){let l=s?.call(this);o?.call(this,n),this.requestUpdate(e,l,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??le}static _$Ei(){if(this.hasOwnProperty(C("elementProperties")))return;let e=Ce(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(C("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(C("properties"))){let t=this.properties,r=[...ke(t),...Te(t)];for(let s of r)this.createProperty(s,t[s])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[r,s]of t)this.elementProperties.set(r,s)}this._$Eh=new Map;for(let[t,r]of this.elementProperties){let s=this._$Eu(t,r);s!==void 0&&this._$Eh.set(s,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let r=new Set(e.flat(1/0).reverse());for(let s of r)t.unshift(W(s))}else e!==void 0&&t.push(W(e));return t}static _$Eu(e,t){let r=t.attribute;return r===!1?void 0:typeof r=="string"?r:typeof e=="string"?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let r of t.keys())this.hasOwnProperty(r)&&(e.set(r,this[r]),delete this[r]);e.size>0&&(this._$Ep=e)}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return ne(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,r){this._$AK(e,r)}_$ET(e,t){let r=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,r);if(s!==void 0&&r.reflect===!0){let o=(r.converter?.toAttribute!==void 0?r.converter:R).toAttribute(t,r.type);this._$Em=e,o==null?this.removeAttribute(s):this.setAttribute(s,o),this._$Em=null}}_$AK(e,t){let r=this.constructor,s=r._$Eh.get(e);if(s!==void 0&&this._$Em!==s){let o=r.getPropertyOptions(s),n=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:R;this._$Em=s;let l=n.fromAttribute(t,o.type);this[s]=l??this._$Ej?.get(s)??l,this._$Em=null}}requestUpdate(e,t,r,s=!1,o){if(e!==void 0){let n=this.constructor;if(s===!1&&(o=this[e]),r??=n.getPropertyOptions(e),!((r.hasChanged??F)(o,t)||r.useDefault&&r.reflect&&o===this._$Ej?.get(e)&&!this.hasAttribute(n._$Eu(e,r))))return;this.C(e,t,r)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(e,t,{useDefault:r,reflect:s,wrapped:o},n){r&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,n??t??this[e]),o!==!0||n!==void 0)||(this._$AL.has(e)||(this.hasUpdated||r||(t=void 0),this._$AL.set(e,t)),s===!0&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[s,o]of this._$Ep)this[s]=o;this._$Ep=void 0}let r=this.constructor.elementProperties;if(r.size>0)for(let[s,o]of r){let{wrapped:n}=o,l=this[s];n!==!0||this._$AL.has(s)||l===void 0||this.C(s,void 0,o,l)}}let e=!1,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(r=>r.hostUpdate?.()),this.update(t)):this._$EM()}catch(r){throw e=!1,this._$EM(),r}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(e){}firstUpdated(e){}};v.elementStyles=[],v.shadowRootOptions={mode:"open"},v[C("elementProperties")]=new Map,v[C("finalized")]=new Map,Me?.({ReactiveElement:v}),(O.reactiveElementVersions??=[]).push("2.1.2");var X=globalThis,ce=i=>i,j=X.trustedTypes,de=j?j.createPolicy("lit-html",{createHTML:i=>i}):void 0,me="$lit$",$=`lit$${Math.random().toFixed(9).slice(2)}$`,_e="?"+$,Ue=`<${_e}>`,S=document,U=()=>S.createComment(""),L=i=>i===null||typeof i!="object"&&typeof i!="function",ee=Array.isArray,Le=i=>ee(i)||typeof i?.[Symbol.iterator]=="function",K=`[ +\f\r]`,M=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,he=/-->/g,pe=/>/g,x=RegExp(`>|${K}(?:([^\\s"'>=/]+)(${K}*=${K}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`,"g"),ue=/'/g,fe=/"/g,ve=/^(?:script|style|textarea|title)$/i,te=i=>(e,...t)=>({_$litType$:i,strings:e,values:t}),c=te(1),Ke=te(2),Ye=te(3),E=Symbol.for("lit-noChange"),u=Symbol.for("lit-nothing"),ge=new WeakMap,A=S.createTreeWalker(S,129);function ye(i,e){if(!ee(i)||!i.hasOwnProperty("raw"))throw Error("invalid template strings array");return de!==void 0?de.createHTML(e):e}var Pe=(i,e)=>{let t=i.length-1,r=[],s,o=e===2?"":e===3?"":"",n=M;for(let l=0;l"?(n=s??M,d=-1):g[1]===void 0?d=-2:(d=n.lastIndex-g[2].length,p=g[1],n=g[3]===void 0?x:g[3]==='"'?fe:ue):n===fe||n===ue?n=x:n===he||n===pe?n=M:(n=x,s=void 0);let y=n===x&&i[l+1].startsWith("/>")?" ":"";o+=n===M?a+Ue:d>=0?(r.push(p),a.slice(0,d)+me+a.slice(d)+$+y):a+$+(d===-2?l:y)}return[ye(i,o+(i[t]||"")+(e===2?"":e===3?"":"")),r]},P=class i{constructor({strings:e,_$litType$:t},r){let s;this.parts=[];let o=0,n=0,l=e.length-1,a=this.parts,[p,g]=Pe(e,t);if(this.el=i.createElement(p,r),A.currentNode=this.el.content,t===2||t===3){let d=this.el.content.firstChild;d.replaceWith(...d.childNodes)}for(;(s=A.nextNode())!==null&&a.length0){s.textContent=j?j.emptyScript:"";for(let y=0;y<_;y++)s.append(d[y],U()),A.nextNode(),a.push({type:2,index:++o});s.append(d[_],U())}}}else if(s.nodeType===8)if(s.data===_e)a.push({type:2,index:o});else{let d=-1;for(;(d=s.data.indexOf($,d+1))!==-1;)a.push({type:7,index:o}),d+=$.length-1}o++}}static createElement(e,t){let r=S.createElement("template");return r.innerHTML=e,r}};function w(i,e,t=i,r){if(e===E)return e;let s=r!==void 0?t._$Co?.[r]:t._$Cl,o=L(e)?void 0:e._$litDirective$;return s?.constructor!==o&&(s?._$AO?.(!1),o===void 0?s=void 0:(s=new o(i),s._$AT(i,t,r)),r!==void 0?(t._$Co??=[])[r]=s:t._$Cl=s),s!==void 0&&(e=w(i,s._$AS(i,e.values),s,r)),e}var Y=class{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){let{el:{content:t},parts:r}=this._$AD,s=(e?.creationScope??S).importNode(t,!0);A.currentNode=s;let o=A.nextNode(),n=0,l=0,a=r[0];for(;a!==void 0;){if(n===a.index){let p;a.type===2?p=new H(o,o.nextSibling,this,e):a.type===1?p=new a.ctor(o,a.name,a.strings,this,e):a.type===6&&(p=new Q(o,this,e)),this._$AV.push(p),a=r[++l]}n!==a?.index&&(o=A.nextNode(),n++)}return A.currentNode=S,s}p(e){let t=0;for(let r of this._$AV)r!==void 0&&(r.strings!==void 0?(r._$AI(e,r,t),t+=r.strings.length-2):r._$AI(e[t])),t++}},H=class i{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,r,s){this.type=2,this._$AH=u,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=r,this.options=s,this._$Cv=s?.isConnected??!0}get parentNode(){let e=this._$AA.parentNode,t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=w(this,e,t),L(e)?e===u||e==null||e===""?(this._$AH!==u&&this._$AR(),this._$AH=u):e!==this._$AH&&e!==E&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):Le(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==u&&L(this._$AH)?this._$AA.nextSibling.data=e:this.T(S.createTextNode(e)),this._$AH=e}$(e){let{values:t,_$litType$:r}=e,s=typeof r=="number"?this._$AC(e):(r.el===void 0&&(r.el=P.createElement(ye(r.h,r.h[0]),this.options)),r);if(this._$AH?._$AD===s)this._$AH.p(t);else{let o=new Y(s,this),n=o.u(this.options);o.p(t),this.T(n),this._$AH=o}}_$AC(e){let t=ge.get(e.strings);return t===void 0&&ge.set(e.strings,t=new P(e)),t}k(e){ee(this._$AH)||(this._$AH=[],this._$AR());let t=this._$AH,r,s=0;for(let o of e)s===t.length?t.push(r=new i(this.O(U()),this.O(U()),this,this.options)):r=t[s],r._$AI(o),s++;s2||r[0]!==""||r[1]!==""?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=u}_$AI(e,t=this,r,s){let o=this.strings,n=!1;if(o===void 0)e=w(this,e,t,0),n=!L(e)||e!==this._$AH&&e!==E,n&&(this._$AH=e);else{let l=e,a,p;for(e=o[0],a=0;a{let r=t?.renderBefore??e,s=r._$litPart$;if(s===void 0){let o=t?.renderBefore??null;r._$litPart$=s=new H(e.insertBefore(U(),o),o,void 0,t??{})}return s._$AI(i),s};var re=globalThis,b=class extends v{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=$e(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return E}};b._$litElement$=!0,b.finalized=!0,re.litElementHydrateSupport?.({LitElement:b});var Ne=re.litElementPolyfillSupport;Ne?.({LitElement:b});(re.litElementVersions??=[]).push("4.2.2");var be=i=>(e,t)=>{t!==void 0?t.addInitializer(()=>{customElements.define(i,e)}):customElements.define(i,e)};var ze={attribute:!0,type:String,converter:R,reflect:!1,hasChanged:F},De=(i=ze,e,t)=>{let{kind:r,metadata:s}=t,o=globalThis.litPropertyMetadata.get(s);if(o===void 0&&globalThis.litPropertyMetadata.set(s,o=new Map),r==="setter"&&((i=Object.create(i)).wrapped=!0),o.set(t.name,i),r==="accessor"){let{name:n}=t;return{set(l){let a=e.get.call(this);e.set.call(this,l),this.requestUpdate(n,a,i,!0,l)},init(l){return l!==void 0&&this.C(n,void 0,i,l),l}}}if(r==="setter"){let{name:n}=t;return function(l){let a=this[n];e.call(this,l),this.requestUpdate(n,a,i,!0,l)}}throw Error("Unsupported decorator location: "+r)};function N(i){return(e,t)=>typeof t=="object"?De(i,e,t):((r,s,o)=>{let n=s.hasOwnProperty(o);return s.constructor.createProperty(o,r),n?Object.getOwnPropertyDescriptor(s,o):void 0})(i,e,t)}function m(i){return N({...i,state:!0,attribute:!1})}function se(i,e){return c`${i.map(t=>Ie(t,e))}`}function Ie(i,e){switch(i.k){case"newline":return c`
`;case"indent":return c`${i.t}`;case"keyword":return c`${i.t}`;case"operator":return c`${i.t}`;case"value":return c`${i.t}`;case"ref":{let t=e&&i.ek&&typeof i.ei=="number"?()=>e(i.ek,i.ei):void 0;return c``}default:return c`${i.t}`}}var Oe=["TIMED","EVENT","YEARLY","WHEN","AT","EVERY","REMARK"],Fe=5e3,h=class extends b{constructor(){super(...arguments);this.narrow=!1;this._entryId=null;this._rows=[];this._total=0;this._filteredTotal=0;this._loading=!1;this._error=null;this._activeTriggerTypes=new Set;this._referenceFilter=null;this._searchTerm="";this._selectedSlot=null;this._detail=null;this._detailLoading=!1;this._fireFeedback=null;this._refreshTimer=null}connectedCallback(){super.connectedCallback(),this._discoverEntry(),this._entryId&&(this._loadList(),this._startRefreshTimer())}disconnectedCallback(){super.disconnectedCallback(),this._stopRefreshTimer()}updated(t){t.has("hass")&&this._entryId===null&&(this._discoverEntry(),this._entryId&&(this._loadList(),this._startRefreshTimer()))}_discoverEntry(){this.hass?.connection&&this._discoverViaList()}async _discoverViaList(){try{let r=(await this.hass.connection.sendMessagePromise({type:"config_entries/get"})).filter(s=>s.domain==="omni_pca");if(r.length===0){this._error="No Omni panel configured. Add one via Settings \u2192 Devices & Services.";return}this._entryId=r[0].entry_id,this._error=null}catch(t){this._error=`Could not discover panels: ${t instanceof Error?t.message:String(t)}`}}async _loadList(){if(this._entryId){this._loading=!0,this._error=null;try{let t={type:"omni_pca/programs/list",entry_id:this._entryId};this._activeTriggerTypes.size>0&&(t.trigger_types=[...this._activeTriggerTypes]),this._referenceFilter&&(t.references_entity=this._referenceFilter),this._searchTerm&&(t.search=this._searchTerm);let r=await this.hass.connection.sendMessagePromise(t);this._rows=r.programs,this._total=r.total,this._filteredTotal=r.filtered_total}catch(t){this._error=t instanceof Error?t.message:String(t)}finally{this._loading=!1}}}async _loadDetail(t){if(this._entryId){this._detailLoading=!0,this._detail=null;try{this._detail=await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/get",entry_id:this._entryId,slot:t})}catch(r){this._error=r instanceof Error?r.message:String(r)}finally{this._detailLoading=!1}}}async _fireProgram(t){if(this._entryId){this._fireFeedback="firing\u2026";try{await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/fire",entry_id:this._entryId,slot:t}),this._fireFeedback=`fired slot ${t}`}catch(r){this._fireFeedback=`error: ${r instanceof Error?r.message:r}`}setTimeout(()=>{this._fireFeedback=null},4e3)}}_startRefreshTimer(){this._refreshTimer===null&&(this._refreshTimer=window.setInterval(()=>{this._loadList(),this._selectedSlot!==null&&this._loadDetail(this._selectedSlot)},Fe))}_stopRefreshTimer(){this._refreshTimer!==null&&(window.clearInterval(this._refreshTimer),this._refreshTimer=null)}_toggleTriggerFilter(t){let r=new Set(this._activeTriggerTypes);r.has(t)?r.delete(t):r.add(t),this._activeTriggerTypes=r,this._loadList()}_onSearchInput(t){this._searchTerm=t.target.value,this._loadList()}_clearReferenceFilter(){this._referenceFilter=null,this._loadList()}_onRowClick(t){this._selectedSlot=t,this._loadDetail(t)}_onRefClick(t,r){this._referenceFilter=`${t}:${r}`,this._selectedSlot=null,this._detail=null,this._loadList()}_closeDetail(){this._selectedSlot=null,this._detail=null}render(){return c` +
+
+ + Omni Programs + ${this._total>0?c` + + ${this._filteredTotal===this._total?`${this._total} programs`:`${this._filteredTotal} of ${this._total} shown`} + `:""} +
+
+ ${this._error?c` +
${this._error}
`:""} + ${this._renderFilters()} +
+ ${this._renderList()} + ${this._selectedSlot!==null?this._renderDetail():""} +
+ `}_renderFilters(){return c` +
+ +
+ ${Oe.map(t=>c` + + `)} +
+ ${this._referenceFilter?c` +
+ filtering on ${this._referenceFilter} + +
`:""} +
+ `}_renderList(){return this._loading&&this._rows.length===0?c`
loading…
`:this._rows.length===0?c`
No programs match the current filters.
`:c` +
+ ${this._rows.map(t=>c` +
this._onRowClick(t.slot)} + > +
#${t.slot}
+
+ ${se(t.summary,(r,s)=>this._onRefClick(r,s))} +
+
+ + ${t.trigger_type} + + ${t.condition_count>0?c` + ${t.condition_count} cond`:""} + ${t.action_count>1?c` + ${t.action_count} actions`:""} +
+
+ `)} +
+ `}_renderDetail(){if(this._detailLoading)return c``;if(this._detail===null)return c``;let t=this._detail;return c` + + `}};h.styles=B` + :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); } - } -} -customElements.define('omni-panel-programs', OmniPanelPrograms); + .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); + } + `,f([N({attribute:!1})],h.prototype,"hass",2),f([N({attribute:!1})],h.prototype,"narrow",2),f([m()],h.prototype,"_entryId",2),f([m()],h.prototype,"_rows",2),f([m()],h.prototype,"_total",2),f([m()],h.prototype,"_filteredTotal",2),f([m()],h.prototype,"_loading",2),f([m()],h.prototype,"_error",2),f([m()],h.prototype,"_activeTriggerTypes",2),f([m()],h.prototype,"_referenceFilter",2),f([m()],h.prototype,"_searchTerm",2),f([m()],h.prototype,"_selectedSlot",2),f([m()],h.prototype,"_detail",2),f([m()],h.prototype,"_detailLoading",2),f([m()],h.prototype,"_fireFeedback",2),h=f([be("omni-panel-programs")],h);export{h as OmniPanelPrograms}; +/*! Bundled license information: + +@lit/reactive-element/css-tag.js: + (** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/reactive-element.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +lit-html/lit-html.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +lit-element/lit-element.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +lit-html/is-server.js: + (** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/custom-element.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/property.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/state.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/event-options.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/base.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/query.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/query-all.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/query-async.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/query-assigned-elements.js: + (** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/query-assigned-nodes.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) +*/ diff --git a/uv.lock b/uv.lock index 5bbd55a..231e668 100644 --- a/uv.lock +++ b/uv.lock @@ -1511,7 +1511,7 @@ wheels = [ [[package]] name = "omni-pca" -version = "2026.5.11" +version = "2026.5.14" source = { editable = "." } dependencies = [ { name = "cryptography" },