mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-04 13:38:43 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ffa7cf17a6 | |||
| 5a5cec9060 | |||
| bb962ba3d8 | |||
| d4a59ff1c7 | |||
| f496d842a6 | |||
| c14d07c845 | |||
| 8a1f2e86bd | |||
| 1454f4d081 | |||
| 1a47c326b2 | |||
| d8e827e08b | |||
| 301c907fa9 | |||
| 1ef3bc94d0 | |||
| ba0367be2f | |||
| c186a31056 | |||
| bf8c92c95e | |||
| 9743117abf | |||
| 5e012973b2 | |||
| ea659f1b33 | |||
| 539803cb5b | |||
| 944d3332d1 | |||
| 4a267d160f | |||
| 7edb9f8164 | |||
| ff814167c6 | |||
| d08d8dde64 | |||
| 5da441ceed | |||
| 22e646a68a | |||
| d0ab20479f |
@@ -7,8 +7,6 @@ description: Home Assistant frontend component patterns. Use when implementing o
|
||||
|
||||
Use this skill when creating or reviewing Home Assistant UI components and common interaction patterns.
|
||||
|
||||
Cross-load `ha-frontend-events` when component work includes event listener typing, custom event dispatch, or event-map declarations.
|
||||
|
||||
## Dialogs
|
||||
|
||||
Open dialogs through the fire-event pattern:
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
name: ha-frontend-events
|
||||
description: Home Assistant frontend event patterns. Use when typing event handlers, using HASSDomEvent types, dispatching with fireEvent, or declaring HASSDomEvents and event maps.
|
||||
---
|
||||
|
||||
# HA Frontend Events
|
||||
|
||||
Use this skill when implementing or reviewing event listeners and custom event contracts. Cross-load `ha-frontend-components` when the work also involves dialogs, forms, alerts, shortcuts, tooltips, panels, Lovelace cards, or buttons.
|
||||
|
||||
## Event Handling
|
||||
|
||||
Use the event types from `src/common/dom/fire_event.ts` instead of plain `Event`, generic `CustomEvent`, or element casts when they express the handler contract:
|
||||
|
||||
- Use `HASSDomCurrentTargetEvent<T>` to read the element on which the listener was registered through `ev.currentTarget`.
|
||||
- Use `HASSDomTargetEvent<T>` only to read the element that originated the event through `ev.target`.
|
||||
- Use `HASSDomEvent<T>` to read a custom event payload through `ev.detail`.
|
||||
- Use `ValueChangedEvent<T>` from `src/types.ts` for the standard `value-changed` event.
|
||||
- Prefer an event type exported by the component being listened to, such as `HaSelectSelectEvent<T, Clearable>` or `HaDropdownSelectEvent<TValue, TData>`, over reconstructing its detail type.
|
||||
- Use `ActionHandlerEvent` from `src/data/lovelace/action_handler.ts` for Lovelace tap, hold, and double-tap handlers.
|
||||
|
||||
Import event and element types with `import type`:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import type { HaCheckbox } from "../components/ha-checkbox";
|
||||
import type { HaEntityPicker } from "../components/entity/ha-entity-picker";
|
||||
import type { HaRadioGroup } from "../components/radio/ha-radio-group";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
```
|
||||
|
||||
Type the handler so the selected property can be read directly. Do not cast `ev.currentTarget` or assign it to a single-use variable:
|
||||
|
||||
```ts
|
||||
private _scopeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>): void {
|
||||
this._scope = ev.currentTarget.value;
|
||||
}
|
||||
|
||||
private _checkedChanged(ev: HASSDomTargetEvent<HaCheckbox>): void {
|
||||
this._checked = ev.target.checked;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>): void {
|
||||
this._value = ev.detail.value;
|
||||
}
|
||||
|
||||
private _itemSelected(ev: HASSDomEvent<{ id: string }>): void {
|
||||
this._selectedId = ev.detail.id;
|
||||
}
|
||||
```
|
||||
|
||||
Use intersections when a handler needs more than one facet of an event. Keep the native event type when the handler reads native fields such as `key`, modifier keys, `dataTransfer`, or focus relationships:
|
||||
|
||||
```ts
|
||||
private _entityChanged(
|
||||
ev: ValueChangedEvent<string> & HASSDomCurrentTargetEvent<HaEntityPicker>
|
||||
): void {
|
||||
ev.currentTarget.value = ev.detail.value;
|
||||
}
|
||||
|
||||
private _keyDown(
|
||||
ev: KeyboardEvent & HASSDomCurrentTargetEvent<HTMLInputElement>
|
||||
): void {
|
||||
if (ev.key === "Enter") {
|
||||
this._submit(ev.currentTarget.value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Dispatch Home Assistant component events with `fireEvent()` instead of constructing `Event` or `CustomEvent` directly. Register the event name and detail type by augmenting `HASSDomEvents`; use `undefined` when an event has no detail. `fireEvent()` constrains event names and supplied detail, and events bubble and cross shadow boundaries by default:
|
||||
|
||||
```ts
|
||||
fireEvent(this, "item-selected", { id: item.id });
|
||||
fireEvent(this, "refresh-requested");
|
||||
```
|
||||
|
||||
When an event is already registered, derive handler and listener types from its registration rather than repeating the payload shape:
|
||||
|
||||
```ts
|
||||
private _itemSelected(
|
||||
ev: HASSDomEvent<HASSDomEvents["item-selected"]>
|
||||
): void {
|
||||
this._selectedId = ev.detail.id;
|
||||
}
|
||||
```
|
||||
|
||||
`HASSDomEvents` types `fireEvent()` calls. Augment `HTMLElementEventMap` for typed listeners on HTML elements, or `GlobalEventHandlersEventMap` when the event is handled on global event targets.
|
||||
|
||||
In component files, prefer placing global event declarations after the class at the bottom of the file. Preserve the existing placement when editing established files; foundational type, helper, and mixin files commonly keep declarations near the top before their consumers.
|
||||
|
||||
```ts
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"item-selected": { id: string };
|
||||
"refresh-requested": undefined;
|
||||
}
|
||||
|
||||
interface HTMLElementEventMap {
|
||||
"item-selected": HASSDomEvent<HASSDomEvents["item-selected"]>;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -70,4 +70,4 @@ Configuration and props:
|
||||
- Identify behavioral regressions, bugs, accessibility issues, and missing tests first.
|
||||
- Keep style-only comments secondary unless they affect maintainability or user experience.
|
||||
- Prefer small, direct fixes over large refactors during review follow-up.
|
||||
- Cross-load `ha-frontend-contexts`, `ha-frontend-components`, `ha-frontend-events`, `ha-frontend-styling`, `ha-frontend-testing`, or `ha-frontend-user-facing-text` when a finding falls in that area.
|
||||
- Cross-load `ha-frontend-contexts`, `ha-frontend-components`, `ha-frontend-styling`, `ha-frontend-testing`, or `ha-frontend-user-facing-text` when a finding falls in that area.
|
||||
|
||||
@@ -20,7 +20,6 @@ yarn lint # ESLint + Prettier + TypeScript + Lit
|
||||
yarn format # Auto-fix ESLint + Prettier
|
||||
yarn lint:types # TypeScript compiler, run without file arguments
|
||||
yarn test # Vitest
|
||||
yarn build # Full production build
|
||||
yarn dev # App dev server
|
||||
yarn dev:serve # Local serving dev server
|
||||
```
|
||||
@@ -29,26 +28,6 @@ Never run `tsc` or `yarn lint:types` with file arguments. File arguments make `t
|
||||
|
||||
For focused type feedback on one file, use editor diagnostics instead of a file-scoped `tsc` command.
|
||||
|
||||
## Production Builds
|
||||
|
||||
Production builds support foreground and managed background execution:
|
||||
|
||||
```bash
|
||||
yarn build # Full foreground build
|
||||
yarn build --background # Full managed background build
|
||||
yarn build --modern # Modern frontend_latest bundle only
|
||||
yarn build --modern --background # Modern managed background build
|
||||
yarn build --status
|
||||
yarn build --logs [--follow]
|
||||
yarn build --stop
|
||||
```
|
||||
|
||||
Use `yarn build --modern --background` for production bundle-size or browser performance comparisons that only need modern browser output. It runs the normal metadata and static preparation, minifies and compresses the modern `frontend_latest` bundle and shared static assets, and generates modern-only entry pages and service workers. It deliberately skips the legacy bundle and its service worker.
|
||||
|
||||
Do not pass `--help`, `--background`, or `--modern` to `script/build_frontend`; that raw script does not parse arguments and always starts the full foreground build. Use `yarn build` for managed builds. App builds and development servers keep exclusive ownership of `hass_frontend/` for their lifetime.
|
||||
|
||||
Managed app, demo, gallery, and E2E app workflows share one lifetime lock, so only one build or development server can run at a time.
|
||||
|
||||
## Unit And Utility Tests
|
||||
|
||||
- Add or update Vitest tests for data processing, utility code, and behavior that can be tested without a browser.
|
||||
@@ -62,7 +41,7 @@ Managed app, demo, gallery, and E2E app workflows share one lifetime lock, so on
|
||||
|
||||
`yarn dev:serve` also serves locally and supports `-c` for the core URL and `-p` for the port. The default is 8124, or 8123 in a devcontainer.
|
||||
|
||||
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. `yarn dev`, `yarn dev:serve`, `yarn dev:demo`, and `yarn dev:gallery` also support `--fetch-translations`; this runs translation fetching, including first-time GitHub device authentication, under the workflow lock before starting the watcher. It works in foreground and background modes. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal. `yarn dev` and `yarn dev:serve` share one managed process slot because both write the app output.
|
||||
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal.
|
||||
|
||||
## Playwright E2E
|
||||
|
||||
@@ -80,7 +59,7 @@ The custom development wrappers use `/__ha_dev_status` to identify and manage th
|
||||
|
||||
Local runs against a watched development server do not always match CI's clean build artifacts, environment, sharding, or worker configuration. Use background servers for the fast iteration loop, but confirm the relevant CI jobs complete successfully before considering E2E changes verified.
|
||||
|
||||
Use `-g "<title>" --project=chromium` to narrow a run. `yarn test:e2e` runs suites sequentially when managed servers are unavailable to prevent cold builds racing over shared generated assets. Run suites directly; piping through output truncation hides progress and failures.
|
||||
Use `-g "<title>" --project=chromium` to narrow a run. `yarn test:e2e` runs all three suites in parallel when every managed server is available, otherwise it runs them sequentially to prevent cold builds racing over shared generated assets. Run suites directly; piping through output truncation hides progress and failures.
|
||||
|
||||
The app suite uses a stripped-down harness for e2e. Demo and gallery use their normal dev servers.
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
build-mode: none
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
name: Prepare container dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.62.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.62.0-noble
|
||||
options: --user 1001
|
||||
defaults:
|
||||
run:
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.62.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.62.0-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.62.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.62.0-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -260,7 +260,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.62.1-noble
|
||||
image: mcr.microsoft.com/playwright:v1.62.0-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@ab22029681aa532bfe7de5774a9972d67bfbd2c0 # master
|
||||
uses: home-assistant/actions/helpers/verify-version@e3fb68ebda13d88a0d695082f471ba2c83d025fb # master
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
+944
File diff suppressed because one or more lines are too long
Vendored
-1000
File diff suppressed because one or more lines are too long
+1
-1
@@ -13,4 +13,4 @@ nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.18.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.17.1.cjs
|
||||
|
||||
@@ -40,7 +40,6 @@ Detailed guidance lives in project skills under `.agents/skills/`. Load the matc
|
||||
|
||||
- `ha-frontend-contexts`: Lit contexts, `hass` migration, and rerender-sensitive state access.
|
||||
- `ha-frontend-components`: dialogs, forms, alerts, shortcuts, tooltips, panels, and Lovelace cards.
|
||||
- `ha-frontend-events`: event handler typing, custom event dispatch, and event-map declarations.
|
||||
- `ha-frontend-styling`: theme variables, spacing tokens, responsive layout, RTL, and view transitions.
|
||||
- `ha-frontend-testing`: lint, typecheck, Vitest, Playwright e2e dev servers, and benchmarks.
|
||||
- `ha-frontend-user-facing-text`: localization, terminology, sentence case, and Home Assistant text style.
|
||||
|
||||
@@ -12,7 +12,7 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
|
||||
- Initial setup: `script/setup`
|
||||
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
|
||||
- Production build: `yarn build`
|
||||
- Production build: `script/build_frontend`
|
||||
- Gallery: `cd gallery && script/develop_gallery`
|
||||
|
||||
## Frontend development
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
// Manage a Home Assistant frontend production build with an agent-friendly
|
||||
// interface, matching build-scripts/dev-server.mjs.
|
||||
//
|
||||
// node build-scripts/build-manager.mjs [--modern] [mode]
|
||||
//
|
||||
// (no mode) Run in the foreground.
|
||||
// --background Start detached, print the pid, then exit and leave it
|
||||
// running.
|
||||
// --status Report whether a managed build is running.
|
||||
// --stop Stop a managed build.
|
||||
// --logs [--follow] Print (or follow) the background build log.
|
||||
//
|
||||
// --modern Build only the modern frontend_latest bundle.
|
||||
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
LIFECYCLE_MODE_FLAGS,
|
||||
acquireProcessRecord,
|
||||
isProcessRecordAlive,
|
||||
outputLog,
|
||||
offerToStopProcessRecord,
|
||||
processStartTime,
|
||||
readProcessRecord,
|
||||
releaseProcessRecord,
|
||||
runCli,
|
||||
spawnDetachedToLog,
|
||||
spawnForeground,
|
||||
terminateDetachedProcess,
|
||||
terminateProcess,
|
||||
waitFor,
|
||||
writeProcessRecord,
|
||||
} from "./managed-process.mjs";
|
||||
import {
|
||||
buildCacheDir,
|
||||
describeOutputOwner,
|
||||
workflowLockEnv,
|
||||
workflowLockFile,
|
||||
} from "./output-lock.mjs";
|
||||
|
||||
const repoRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
".."
|
||||
);
|
||||
const gulpBin = path.join(repoRoot, "node_modules", ".bin", "gulp");
|
||||
const stateDir = path.join(buildCacheDir, "ha-build");
|
||||
const logFile = path.join(stateDir, "build.log");
|
||||
const lockFile = workflowLockFile;
|
||||
|
||||
const usage = () => {
|
||||
process.stderr.write(
|
||||
"Usage: node build-scripts/build-manager.mjs [--modern] " +
|
||||
"[--background | --status | --stop | --logs [--follow]]\n"
|
||||
);
|
||||
};
|
||||
|
||||
const parseArgs = (argv) => {
|
||||
const args = {
|
||||
mode: "foreground",
|
||||
modes: [],
|
||||
follow: false,
|
||||
modern: false,
|
||||
unknown: [],
|
||||
};
|
||||
for (const arg of argv) {
|
||||
if (LIFECYCLE_MODE_FLAGS.has(arg)) {
|
||||
args.mode = LIFECYCLE_MODE_FLAGS.get(arg);
|
||||
args.modes.push(arg);
|
||||
} else if (arg === "--modern") {
|
||||
args.modern = true;
|
||||
} else if (arg === "--follow") {
|
||||
args.follow = true;
|
||||
} else {
|
||||
args.unknown.push(arg);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
};
|
||||
|
||||
const hints = () =>
|
||||
" Stop: yarn build --stop\n" +
|
||||
" Status: yarn build --status\n" +
|
||||
" Logs: yarn build --logs\n";
|
||||
|
||||
const devCommand = (suite) => {
|
||||
switch (suite) {
|
||||
case "app-serve":
|
||||
return "dev:serve";
|
||||
case "demo":
|
||||
return "dev:demo";
|
||||
case "gallery":
|
||||
return "dev:gallery";
|
||||
case "e2e-app":
|
||||
return "test:e2e:app:dev";
|
||||
default:
|
||||
return "dev";
|
||||
}
|
||||
};
|
||||
|
||||
const readBuild = () => readProcessRecord(lockFile);
|
||||
|
||||
const releaseBuild = (token) => releaseProcessRecord(lockFile, token);
|
||||
|
||||
const stopCommandFor = (owner) =>
|
||||
owner?.kind === "build"
|
||||
? "yarn build --stop"
|
||||
: owner?.kind === "dev"
|
||||
? `yarn ${devCommand(owner.suite)} --stop`
|
||||
: undefined;
|
||||
|
||||
const acquireBuild = async (modern, foreground) => {
|
||||
const token = `${process.pid}-${Date.now()}-${Math.random()}`;
|
||||
const record = {
|
||||
pid: process.pid,
|
||||
startTime: processStartTime(process.pid),
|
||||
processGroup: false,
|
||||
foreground,
|
||||
kind: "build",
|
||||
modern,
|
||||
starting: true,
|
||||
token,
|
||||
};
|
||||
const result = acquireProcessRecord(lockFile, record);
|
||||
if (result.acquired) {
|
||||
return { token };
|
||||
}
|
||||
reportExisting(result.existing);
|
||||
return (await offerToStopProcessRecord({
|
||||
file: lockFile,
|
||||
owner: result.existing,
|
||||
ownerDescription: describeOutputOwner(result.existing),
|
||||
stopCommand: stopCommandFor(result.existing),
|
||||
}))
|
||||
? acquireBuild(modern, foreground)
|
||||
: { existing: result.existing };
|
||||
};
|
||||
|
||||
const updateBuild = (token, child, processGroup) => {
|
||||
const existing = readBuild();
|
||||
if (existing?.token !== token) {
|
||||
throw Error("Frontend build lock ownership was lost during startup.");
|
||||
}
|
||||
writeProcessRecord(lockFile, {
|
||||
...existing,
|
||||
pid: child.pid,
|
||||
startTime: processStartTime(child.pid),
|
||||
processGroup,
|
||||
starting: false,
|
||||
});
|
||||
};
|
||||
|
||||
const taskFor = (modern) => (modern ? "build-app-modern" : "build-app");
|
||||
|
||||
const reportExisting = (existing) => {
|
||||
if (existing?.kind === "output") {
|
||||
process.stdout.write(
|
||||
`${describeOutputOwner(existing)} already owns the build and development workflow` +
|
||||
`${existing.pid ? ` (pid ${existing.pid})` : ""}.\n`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (existing?.kind === "dev") {
|
||||
const command = devCommand(existing.suite);
|
||||
process.stdout.write(
|
||||
`Dev server (${existing.suite}) already running` +
|
||||
`${existing.pid ? ` (pid ${existing.pid})` : ""}.\n` +
|
||||
` Stop: yarn ${command} --stop\n` +
|
||||
` Status: yarn ${command} --status\n` +
|
||||
` Logs: yarn ${command} --logs\n`
|
||||
);
|
||||
return;
|
||||
}
|
||||
process.stdout.write(
|
||||
`Frontend ${existing?.modern ? "modern " : ""}build already running` +
|
||||
`${existing?.pid ? ` (pid ${existing.pid})` : ""}.\n${hints()}`
|
||||
);
|
||||
};
|
||||
|
||||
const runForeground = async (modern) => {
|
||||
const lock = await acquireBuild(modern, true);
|
||||
if (!lock.token) {
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
return await spawnForeground({
|
||||
cmd: gulpBin,
|
||||
args: [taskFor(modern)],
|
||||
cwd: repoRoot,
|
||||
env: workflowLockEnv(lock.token),
|
||||
processGroup: true,
|
||||
onSpawn: (child) => updateBuild(lock.token, child, true),
|
||||
});
|
||||
} finally {
|
||||
releaseBuild(lock.token);
|
||||
}
|
||||
};
|
||||
|
||||
const runBackground = async (modern) => {
|
||||
const lock = await acquireBuild(modern, false);
|
||||
if (!lock.token) {
|
||||
return 1;
|
||||
}
|
||||
let child;
|
||||
try {
|
||||
child = await spawnDetachedToLog({
|
||||
cmd: gulpBin,
|
||||
args: [taskFor(modern)],
|
||||
cwd: repoRoot,
|
||||
env: workflowLockEnv(lock.token),
|
||||
logFile,
|
||||
});
|
||||
updateBuild(lock.token, child, true);
|
||||
process.stdout.write(
|
||||
`Started ${modern ? "modern " : ""}frontend build (pid ${child.pid})\n` +
|
||||
hints()
|
||||
);
|
||||
return 0;
|
||||
} catch (err) {
|
||||
if (child) {
|
||||
await terminateDetachedProcess(child);
|
||||
}
|
||||
releaseBuild(lock.token);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const runStatus = () => {
|
||||
const existing = readBuild();
|
||||
if (existing?.kind === "build" && isProcessRecordAlive(existing)) {
|
||||
process.stdout.write(
|
||||
`Frontend ${existing.modern ? "modern " : ""}build running (pid ${existing.pid}).\n`
|
||||
);
|
||||
} else {
|
||||
if (existing?.kind === "build") {
|
||||
releaseBuild(existing.token);
|
||||
}
|
||||
process.stdout.write("Frontend build not running.\n");
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const runStop = async () => {
|
||||
let existing = readBuild();
|
||||
if (existing?.kind !== "build") {
|
||||
process.stdout.write("Frontend build not running.\n");
|
||||
return 0;
|
||||
}
|
||||
if (existing?.starting) {
|
||||
const token = existing.token;
|
||||
await waitFor(
|
||||
() => {
|
||||
const current = readBuild();
|
||||
return !current?.starting || current.token !== token;
|
||||
},
|
||||
100,
|
||||
5000
|
||||
);
|
||||
existing = readBuild();
|
||||
}
|
||||
if (
|
||||
!existing ||
|
||||
existing.kind !== "build" ||
|
||||
!isProcessRecordAlive(existing)
|
||||
) {
|
||||
if (existing) releaseBuild(existing.token);
|
||||
process.stdout.write("Frontend build not running.\n");
|
||||
return 0;
|
||||
}
|
||||
if (
|
||||
!(await terminateProcess({
|
||||
pid: existing.pid,
|
||||
processGroup: existing.processGroup,
|
||||
isStopped: () => !isProcessRecordAlive(existing),
|
||||
}))
|
||||
) {
|
||||
process.stderr.write(
|
||||
`Failed to stop frontend build (pid ${existing.pid}). Stop it manually.\n`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
releaseBuild(existing.token);
|
||||
process.stdout.write(`Stopped frontend build (pid ${existing.pid}).\n`);
|
||||
return 0;
|
||||
};
|
||||
|
||||
const runLogs = (follow) =>
|
||||
outputLog(logFile, follow, `No frontend build log yet (${logFile}).\n`);
|
||||
|
||||
const main = async () => {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.unknown.length) {
|
||||
process.stderr.write(`Unknown arguments: ${args.unknown.join(" ")}\n`);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
if (args.modes.length > 1 || (args.follow && args.mode !== "logs")) {
|
||||
process.stderr.write("Invalid combination of build arguments.\n");
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
const handlers = {
|
||||
foreground: () => runForeground(args.modern),
|
||||
background: () => runBackground(args.modern),
|
||||
status: runStatus,
|
||||
stop: runStop,
|
||||
logs: () => runLogs(args.follow),
|
||||
};
|
||||
return handlers[args.mode]();
|
||||
};
|
||||
|
||||
runCli(main);
|
||||
+413
-496
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
import gulp from "gulp";
|
||||
import env from "../env.cjs";
|
||||
import { createWorkflowLockTask } from "../output-lock.mjs";
|
||||
import "./clean.js";
|
||||
import "./compress.js";
|
||||
import "./entry-html.js";
|
||||
@@ -18,7 +17,6 @@ gulp.task(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
createWorkflowLockTask("develop-app"),
|
||||
"clean",
|
||||
gulp.parallel(
|
||||
"gen-service-worker-app-dev",
|
||||
@@ -38,7 +36,6 @@ gulp.task(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
createWorkflowLockTask("build-app"),
|
||||
"clean",
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
@@ -54,30 +51,6 @@ gulp.task(
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-app-modern",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
createWorkflowLockTask("build-app-modern"),
|
||||
"clean",
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"build-translations",
|
||||
"build-locale-data",
|
||||
"gen-licenses"
|
||||
),
|
||||
"copy-static-app",
|
||||
"rspack-prod-app-modern",
|
||||
gulp.parallel(
|
||||
"gen-pages-app-prod-modern",
|
||||
"gen-service-worker-app-prod-modern"
|
||||
),
|
||||
...(env.isTestBuild() || env.isStatsBuild() ? [] : ["compress-app"])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"analyze-app",
|
||||
gulp.series(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import gulp from "gulp";
|
||||
import { createWorkflowLockTask } from "../output-lock.mjs";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
@@ -14,7 +13,6 @@ gulp.task(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
createWorkflowLockTask("develop-demo"),
|
||||
"clean-demo",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(
|
||||
@@ -34,7 +32,6 @@ gulp.task(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
createWorkflowLockTask("build-demo"),
|
||||
"clean-demo",
|
||||
// Cast needs to be backwards compatible and older HA has no translations
|
||||
"translations-enable-merge-backend",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import gulp from "gulp";
|
||||
import { createWorkflowLockTask } from "../output-lock.mjs";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
@@ -13,7 +12,6 @@ gulp.task(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
createWorkflowLockTask("develop-e2e-test-app"),
|
||||
"clean-e2e-test-app",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(
|
||||
@@ -33,7 +31,6 @@ gulp.task(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
createWorkflowLockTask("build-e2e-test-app"),
|
||||
"clean-e2e-test-app",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
|
||||
@@ -58,12 +58,6 @@ const getCommonTemplateVars = () => {
|
||||
return {
|
||||
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
|
||||
hassUrl: process.env.HASS_URL || "",
|
||||
// Single source for the stale-build recovery patterns, shared with the
|
||||
// bundled src/util/recover-stale-build.ts and injected into the inline
|
||||
// boot guard (_bootstrap_recovery.html.template).
|
||||
staleBuildPatterns: fs.readJsonSync(
|
||||
resolve(paths.root_dir, "src/util/stale-build-patterns.json")
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -113,9 +107,6 @@ const genPagesDevTask =
|
||||
resolve(inputRoot, inputSub, `${page}.template`),
|
||||
{
|
||||
...commonVars,
|
||||
// Dev entries are unhashed, so the stale-index recovery guard has
|
||||
// nothing to key off and rebuild churn could cause spurious reloads.
|
||||
useCacheRecovery: false,
|
||||
latestEntryJS: entries.map(
|
||||
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
|
||||
),
|
||||
@@ -155,15 +146,10 @@ const genPagesProdTask =
|
||||
resolve(inputRoot, inputSub, `${page}.template`),
|
||||
{
|
||||
...commonVars,
|
||||
// Recover from a stale index.html that pins deleted hashed entry
|
||||
// bundles (see _bootstrap_recovery.html.template).
|
||||
useCacheRecovery: true,
|
||||
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
|
||||
es5EntryJS: outputES5
|
||||
? entries.map((entry) => es5Manifest[`${entry}.js`])
|
||||
: [],
|
||||
es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]),
|
||||
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
||||
es5CustomPanelJS: outputES5 ? es5Manifest["custom-panel.js"] : "",
|
||||
es5CustomPanelJS: es5Manifest["custom-panel.js"],
|
||||
}
|
||||
);
|
||||
minifiedHTML.push(
|
||||
@@ -198,17 +184,6 @@ gulp.task(
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-app-prod-modern",
|
||||
genPagesProdTask(
|
||||
APP_PAGE_ENTRIES,
|
||||
paths.root_dir,
|
||||
paths.app_output_root,
|
||||
paths.app_output_latest,
|
||||
undefined
|
||||
)
|
||||
);
|
||||
|
||||
const CAST_PAGE_ENTRIES = {
|
||||
"faq.html": ["launcher"],
|
||||
"index.html": ["launcher"],
|
||||
|
||||
@@ -4,7 +4,6 @@ import gulp from "gulp";
|
||||
import { load as loadYaml } from "js-yaml";
|
||||
import { marked } from "marked";
|
||||
import path from "path";
|
||||
import { createWorkflowLockTask } from "../output-lock.mjs";
|
||||
import paths from "../paths.cjs";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
@@ -165,7 +164,6 @@ gulp.task(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
createWorkflowLockTask("develop-gallery"),
|
||||
"clean-gallery",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(
|
||||
@@ -197,7 +195,6 @@ gulp.task(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
createWorkflowLockTask("build-gallery"),
|
||||
"clean-gallery",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(
|
||||
|
||||
@@ -108,7 +108,7 @@ const runDevServer = async ({
|
||||
}
|
||||
};
|
||||
|
||||
const doneHandler = () => (err, stats) => {
|
||||
const doneHandler = (done) => (err, stats) => {
|
||||
if (err) {
|
||||
log.error(err.stack || err);
|
||||
if (err.details) {
|
||||
@@ -122,26 +122,18 @@ const doneHandler = () => (err, stats) => {
|
||||
}
|
||||
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
||||
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
const prodBuild = (conf) =>
|
||||
new Promise((resolve, reject) => {
|
||||
new Promise((resolve) => {
|
||||
rspack(
|
||||
conf,
|
||||
// Resolve promise when done. Because we pass a callback, rspack closes itself
|
||||
(err, stats) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (stats.hasErrors()) {
|
||||
reject(Error(stats.toString("errors-only")));
|
||||
} else {
|
||||
if (stats.hasWarnings()) {
|
||||
console.log(stats.toString("minimal"));
|
||||
}
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
doneHandler(resolve)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -168,17 +160,6 @@ gulp.task("rspack-prod-app", () =>
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-prod-app-modern", () =>
|
||||
prodBuild(
|
||||
createAppConfig({
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
latestBuild: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-dev-server-demo", () =>
|
||||
runDevServer({
|
||||
compiler: rspack(
|
||||
|
||||
@@ -34,9 +34,9 @@ gulp.task("gen-service-worker-app-dev", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
const genServiceWorker = (builds) =>
|
||||
gulp.task("gen-service-worker-app-prod", () =>
|
||||
Promise.all(
|
||||
builds.map(async ([outPath, build]) => {
|
||||
Object.entries(SW_MAP).map(async ([outPath, build]) => {
|
||||
const manifest = JSON.parse(
|
||||
await readFile(join(outPath, "manifest.json"), "utf-8")
|
||||
);
|
||||
@@ -83,12 +83,5 @@ const genServiceWorker = (builds) =>
|
||||
await symlink(basename(swDest), swOld);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task("gen-service-worker-app-prod", () =>
|
||||
genServiceWorker(Object.entries(SW_MAP))
|
||||
);
|
||||
|
||||
gulp.task("gen-service-worker-app-prod-modern", () =>
|
||||
genServiceWorker([[paths.app_output_latest, "modern"]])
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { deleteAsync } from "del";
|
||||
import { glob } from "glob";
|
||||
import gulp from "gulp";
|
||||
|
||||
@@ -1,552 +0,0 @@
|
||||
import { spawn, execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
|
||||
export const LIFECYCLE_MODE_FLAGS = new Map([
|
||||
["--background", "background"],
|
||||
["--status", "status"],
|
||||
["--stop", "stop"],
|
||||
["--logs", "logs"],
|
||||
]);
|
||||
|
||||
const AGENT_PROVIDERS = [
|
||||
{
|
||||
id: "opencode",
|
||||
env: [
|
||||
"OPENCODE",
|
||||
"OPENCODE_BIN_PATH",
|
||||
"OPENCODE_SERVER",
|
||||
"OPENCODE_APP_INFO",
|
||||
"OPENCODE_MODES",
|
||||
],
|
||||
processes: ["opencode"],
|
||||
},
|
||||
{
|
||||
id: "claude-code",
|
||||
env: ["CLAUDECODE"],
|
||||
processes: ["claude"],
|
||||
},
|
||||
{
|
||||
id: "cursor",
|
||||
env: ["CURSOR_TRACE_ID"],
|
||||
processes: [],
|
||||
},
|
||||
{
|
||||
id: "github-copilot",
|
||||
matchesEnv: (env) =>
|
||||
env.TERM_PROGRAM === "vscode" && env.GIT_PAGER === "cat",
|
||||
processes: [],
|
||||
},
|
||||
{
|
||||
id: "generic",
|
||||
env: ["AGENT", "AI_AGENT"],
|
||||
processes: [],
|
||||
},
|
||||
];
|
||||
const MAX_ANCESTRY_HOPS = 24;
|
||||
|
||||
const readProcessParent = (pid) => {
|
||||
try {
|
||||
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
||||
const commandEnd = stat.lastIndexOf(")");
|
||||
const commandStart = stat.indexOf("(");
|
||||
if (commandStart === -1 || commandEnd === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const parentPid = Number.parseInt(
|
||||
stat.slice(commandEnd + 2).split(" ")[1] ?? "",
|
||||
10
|
||||
);
|
||||
return Number.isFinite(parentPid)
|
||||
? {
|
||||
command: stat.slice(commandStart + 1, commandEnd).toLowerCase(),
|
||||
parentPid,
|
||||
}
|
||||
: undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const detectCodingAgent = (env = process.env, pid = process.pid) => {
|
||||
if (env.HA_CODING_AGENT === "0") {
|
||||
return undefined;
|
||||
}
|
||||
const envMatch = AGENT_PROVIDERS.find(
|
||||
(provider) =>
|
||||
provider.matchesEnv?.(env) || provider.env?.some((name) => env[name])
|
||||
);
|
||||
if (envMatch) {
|
||||
return envMatch.id;
|
||||
}
|
||||
if (env.HA_CODING_AGENT === "1") {
|
||||
return "unknown";
|
||||
}
|
||||
let currentPid = pid;
|
||||
for (let hop = 0; hop < MAX_ANCESTRY_HOPS; hop++) {
|
||||
const processInfo = readProcessParent(currentPid);
|
||||
if (!processInfo) {
|
||||
return undefined;
|
||||
}
|
||||
const processMatch = AGENT_PROVIDERS.find((provider) =>
|
||||
provider.processes.some((name) => processInfo.command.includes(name))
|
||||
);
|
||||
if (processMatch) {
|
||||
return processMatch.id;
|
||||
}
|
||||
if (processInfo.parentPid <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
currentPid = processInfo.parentPid;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const canPromptForConflict = () =>
|
||||
Boolean(process.stdin.isTTY && process.stderr.isTTY && !detectCodingAgent());
|
||||
|
||||
const formatCommand = (command) =>
|
||||
!("NO_COLOR" in process.env)
|
||||
? `\u001b[1;36m${command}\u001b[0m`
|
||||
: `\`${command}\``;
|
||||
|
||||
const confirmStopConflict = async (ownerDescription, stopCommand) => {
|
||||
const readline = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stderr,
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 30_000);
|
||||
try {
|
||||
process.stderr.write(
|
||||
`${ownerDescription} can be stopped with ${formatCommand(stopCommand)}.\n`
|
||||
);
|
||||
const answer = await readline.question("Stop it and continue? [y/N] ", {
|
||||
signal: controller.signal,
|
||||
});
|
||||
return ["y", "yes"].includes(answer.trim().toLowerCase());
|
||||
} catch (err) {
|
||||
if (err?.name !== "AbortError") {
|
||||
throw err;
|
||||
}
|
||||
process.stderr.write("\n");
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
readline.close();
|
||||
}
|
||||
};
|
||||
|
||||
export const sleep = (ms) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
export const waitFor = async (predicate, intervalMs, timeoutMs) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const poll = async () => {
|
||||
if (await predicate()) {
|
||||
return true;
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
return false;
|
||||
}
|
||||
await sleep(intervalMs);
|
||||
return poll();
|
||||
};
|
||||
return poll();
|
||||
};
|
||||
|
||||
export const isProcessAlive = (pid) => {
|
||||
if (!Number.isInteger(pid) || pid <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (err) {
|
||||
// EPERM means the process exists but is owned by someone else.
|
||||
return err.code === "EPERM";
|
||||
}
|
||||
};
|
||||
|
||||
export const processStartTime = (pid) => {
|
||||
if (!isProcessAlive(pid)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
||||
return stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19];
|
||||
} catch {
|
||||
try {
|
||||
return execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const isProcessRecordAlive = ({ pid, startTime }) =>
|
||||
Boolean(startTime) && processStartTime(pid) === startTime;
|
||||
|
||||
export const readProcessRecord = (file) => {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
return data && Number.isInteger(data.pid) ? data : undefined;
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT" || err instanceof SyntaxError) {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const writeProcessRecord = (file, data) => {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
||||
try {
|
||||
fs.writeFileSync(temporary, JSON.stringify(data));
|
||||
fs.renameSync(temporary, file);
|
||||
} finally {
|
||||
removeFileIfExists(temporary);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeProcessRecord = (file) => {
|
||||
removeFileIfExists(file);
|
||||
};
|
||||
|
||||
export const acquireProcessRecord = (file, data) => {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
try {
|
||||
const fd = fs.openSync(file, "wx");
|
||||
try {
|
||||
fs.writeFileSync(fd, JSON.stringify(data));
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
return { acquired: true };
|
||||
} catch (err) {
|
||||
if (err.code !== "EEXIST") {
|
||||
throw err;
|
||||
}
|
||||
const existing = readProcessRecord(file);
|
||||
if (existing && isProcessRecordAlive(existing)) {
|
||||
return { acquired: false, existing };
|
||||
}
|
||||
if (!existing && isRecentFile(file)) {
|
||||
return { acquired: false };
|
||||
}
|
||||
const removed = withExclusiveFileLockSync(`${file}.cleanup`, () => {
|
||||
const current = readProcessRecord(file);
|
||||
if (
|
||||
current &&
|
||||
(current.token !== existing?.token || isProcessRecordAlive(current))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
removeProcessRecord(file);
|
||||
return true;
|
||||
});
|
||||
if (!removed.acquired || !removed.value) {
|
||||
return { acquired: false, existing: readProcessRecord(file) };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { acquired: false, existing: readProcessRecord(file) };
|
||||
};
|
||||
|
||||
export const releaseProcessRecord = (file, token, onRelease) => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
withExclusiveFileLockSync(`${file}.cleanup`, () => {
|
||||
const existing = readProcessRecord(file);
|
||||
if (!existing || existing.token === token) {
|
||||
onRelease?.();
|
||||
if (existing) {
|
||||
removeProcessRecord(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeFileIfExists = (file) => {
|
||||
try {
|
||||
fs.rmSync(file);
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isRecentFile = (file) => {
|
||||
try {
|
||||
return Date.now() - fs.statSync(file).mtimeMs < 5000;
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const withExclusiveFileLockSync = (file, operation) => {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
let fd;
|
||||
try {
|
||||
fd = fs.openSync(file, "wx");
|
||||
} catch (err) {
|
||||
if (err.code !== "EEXIST") {
|
||||
throw err;
|
||||
}
|
||||
const owner = readProcessRecord(file);
|
||||
if (owner && isProcessRecordAlive(owner)) {
|
||||
return { acquired: false };
|
||||
}
|
||||
if (!owner && isRecentFile(file)) {
|
||||
return { acquired: false };
|
||||
}
|
||||
removeFileIfExists(file);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
fd,
|
||||
JSON.stringify({
|
||||
pid: process.pid,
|
||||
startTime: processStartTime(process.pid),
|
||||
})
|
||||
);
|
||||
return { acquired: true, value: operation() };
|
||||
} finally {
|
||||
try {
|
||||
fs.closeSync(fd);
|
||||
} finally {
|
||||
removeFileIfExists(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { acquired: false };
|
||||
};
|
||||
|
||||
export const spawnForeground = ({
|
||||
cmd,
|
||||
args,
|
||||
cwd,
|
||||
env,
|
||||
processGroup = false,
|
||||
onSpawn,
|
||||
}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const child = spawn(cmd, args, {
|
||||
cwd,
|
||||
detached: processGroup,
|
||||
env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
let settled = false;
|
||||
const forwardSigint = () =>
|
||||
signalProcess(child.pid, "SIGINT", processGroup);
|
||||
const forwardSigterm = () =>
|
||||
signalProcess(child.pid, "SIGTERM", processGroup);
|
||||
const forwardSighup = () =>
|
||||
signalProcess(child.pid, "SIGHUP", processGroup);
|
||||
const removeSignalHandlers = () => {
|
||||
process.off("SIGINT", forwardSigint);
|
||||
process.off("SIGTERM", forwardSigterm);
|
||||
process.off("SIGHUP", forwardSighup);
|
||||
};
|
||||
if (processGroup) {
|
||||
process.on("SIGINT", forwardSigint);
|
||||
process.on("SIGTERM", forwardSigterm);
|
||||
process.on("SIGHUP", forwardSighup);
|
||||
}
|
||||
child.once("spawn", () => {
|
||||
try {
|
||||
onSpawn?.(child);
|
||||
} catch (err) {
|
||||
settled = true;
|
||||
signalProcess(child.pid, "SIGTERM", processGroup);
|
||||
removeSignalHandlers();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.once("error", (err) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
removeSignalHandlers();
|
||||
process.stderr.write(`Failed to start ${cmd}: ${err.message}\n`);
|
||||
resolve(1);
|
||||
}
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
removeSignalHandlers();
|
||||
resolve(code ?? 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const spawnDetachedToLog = ({ cmd, args, cwd, env, logFile }) =>
|
||||
new Promise((resolve, reject) => {
|
||||
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
||||
const fd = fs.openSync(logFile, "w");
|
||||
let child;
|
||||
try {
|
||||
child = spawn(cmd, args, {
|
||||
cwd,
|
||||
detached: true,
|
||||
env,
|
||||
stdio: ["ignore", fd, fd],
|
||||
});
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
child.once("spawn", () => {
|
||||
child.unref();
|
||||
resolve(child);
|
||||
});
|
||||
child.once("error", reject);
|
||||
});
|
||||
|
||||
const signalProcess = (pid, signal, processGroup) => {
|
||||
if (processGroup) {
|
||||
try {
|
||||
process.kill(-pid, signal);
|
||||
return;
|
||||
} catch {
|
||||
// Fall back to the process itself.
|
||||
}
|
||||
}
|
||||
try {
|
||||
process.kill(pid, signal);
|
||||
} catch {
|
||||
// Already gone.
|
||||
}
|
||||
};
|
||||
|
||||
export const terminateProcess = async ({
|
||||
pid,
|
||||
isStopped,
|
||||
processGroup = true,
|
||||
graceMs = 10_000,
|
||||
}) => {
|
||||
signalProcess(pid, "SIGTERM", processGroup);
|
||||
if (await waitFor(isStopped, 300, graceMs)) {
|
||||
return true;
|
||||
}
|
||||
signalProcess(pid, "SIGKILL", processGroup);
|
||||
await sleep(300);
|
||||
return await isStopped();
|
||||
};
|
||||
|
||||
const sameProcessRecord = (current, expected) =>
|
||||
Boolean(expected?.token) && current?.token === expected.token;
|
||||
|
||||
const stopProcessRecord = async (file, owner) => {
|
||||
let current = readProcessRecord(file);
|
||||
if (!current) {
|
||||
return true;
|
||||
}
|
||||
if (!sameProcessRecord(current, owner)) {
|
||||
return false;
|
||||
}
|
||||
if (!isProcessRecordAlive(current)) {
|
||||
releaseProcessRecord(file, current.token);
|
||||
return true;
|
||||
}
|
||||
const stopped = await terminateProcess({
|
||||
pid: current.pid,
|
||||
processGroup: current.processGroup ?? false,
|
||||
isStopped: () => {
|
||||
const latest = readProcessRecord(file);
|
||||
return (
|
||||
!sameProcessRecord(latest, owner) ||
|
||||
!latest ||
|
||||
!isProcessRecordAlive(latest)
|
||||
);
|
||||
},
|
||||
});
|
||||
current = readProcessRecord(file);
|
||||
if (current && !sameProcessRecord(current, owner)) {
|
||||
return false;
|
||||
}
|
||||
if (!stopped && current) {
|
||||
return false;
|
||||
}
|
||||
releaseProcessRecord(file, owner.token);
|
||||
return true;
|
||||
};
|
||||
|
||||
export const offerToStopProcessRecord = async ({
|
||||
file,
|
||||
owner,
|
||||
ownerDescription,
|
||||
stopCommand,
|
||||
}) => {
|
||||
if (
|
||||
!owner ||
|
||||
!stopCommand ||
|
||||
!canPromptForConflict() ||
|
||||
!(await confirmStopConflict(ownerDescription, stopCommand))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return stopProcessRecord(file, owner);
|
||||
};
|
||||
|
||||
export const terminateDetachedProcess = (child) =>
|
||||
terminateProcess({
|
||||
pid: child.pid,
|
||||
processGroup: true,
|
||||
isStopped: () => !isProcessAlive(child.pid),
|
||||
});
|
||||
|
||||
export const outputLog = (logFile, follow, missingMessage) => {
|
||||
if (!fs.existsSync(logFile)) {
|
||||
process.stdout.write(missingMessage);
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
if (!follow) {
|
||||
process.stdout.write(fs.readFileSync(logFile, "utf8"));
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const tail = spawn("tail", ["-f", logFile], { stdio: "inherit" });
|
||||
let settled = false;
|
||||
tail.once("error", () => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
process.stdout.write(fs.readFileSync(logFile, "utf8"));
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
tail.once("exit", (code) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
resolve(code ?? 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const runCli = (main) => {
|
||||
main().then(
|
||||
(code) => {
|
||||
process.exitCode = code;
|
||||
},
|
||||
(err) => {
|
||||
process.stderr.write(`${err?.stack || err}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
acquireProcessRecord,
|
||||
processStartTime,
|
||||
readProcessRecord,
|
||||
releaseProcessRecord,
|
||||
} from "./managed-process.mjs";
|
||||
|
||||
const repoRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
".."
|
||||
);
|
||||
export const buildCacheDir =
|
||||
process.env.HA_BUILD_CACHE_DIR ??
|
||||
path.join(repoRoot, "node_modules", ".cache");
|
||||
|
||||
const WORKFLOW_LOCK_TOKEN_ENV = "HA_WORKFLOW_LOCK_TOKEN";
|
||||
const signalCleanups = new Set();
|
||||
const cleanupSignals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
||||
|
||||
const handleSignal = (signal) => {
|
||||
for (const cleanup of signalCleanups) {
|
||||
cleanup();
|
||||
}
|
||||
for (const cleanupSignal of cleanupSignals) {
|
||||
process.off(cleanupSignal, handleSignal);
|
||||
}
|
||||
process.kill(process.pid, signal);
|
||||
};
|
||||
|
||||
const registerSignalCleanup = (cleanup) => {
|
||||
if (signalCleanups.size === 0) {
|
||||
for (const signal of cleanupSignals) {
|
||||
process.on(signal, handleSignal);
|
||||
}
|
||||
}
|
||||
signalCleanups.add(cleanup);
|
||||
};
|
||||
|
||||
const unregisterSignalCleanup = (cleanup) => {
|
||||
signalCleanups.delete(cleanup);
|
||||
if (signalCleanups.size === 0) {
|
||||
for (const signal of cleanupSignals) {
|
||||
process.off(signal, handleSignal);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const workflowLockFile = path.join(buildCacheDir, "ha-workflow.lock");
|
||||
|
||||
export const workflowLockEnv = (token) => ({
|
||||
...process.env,
|
||||
[WORKFLOW_LOCK_TOKEN_ENV]: token,
|
||||
});
|
||||
|
||||
export const describeOutputOwner = (owner) => {
|
||||
if (owner?.kind === "build") {
|
||||
return `frontend ${owner.modern ? "modern " : ""}build`;
|
||||
}
|
||||
if (owner?.kind === "dev") {
|
||||
return `dev server (${owner.suite ?? "app"})`;
|
||||
}
|
||||
return owner?.target ? `Gulp task ${owner.target}` : "another process";
|
||||
};
|
||||
|
||||
const createLockTask = ({ file, inheritedTokenEnv, kind, label, target }) => {
|
||||
let exitToken;
|
||||
|
||||
const cleanup = () => {
|
||||
if (!exitToken) {
|
||||
return;
|
||||
}
|
||||
releaseProcessRecord(file, exitToken);
|
||||
exitToken = undefined;
|
||||
process.off("exit", cleanup);
|
||||
unregisterSignalCleanup(cleanup);
|
||||
};
|
||||
const acquire = async () => {
|
||||
const inheritedToken = process.env[inheritedTokenEnv];
|
||||
if (inheritedToken) {
|
||||
if (readProcessRecord(file)?.token !== inheritedToken) {
|
||||
throw Error(
|
||||
`${label} lock ownership was lost before ${target} started.`
|
||||
);
|
||||
}
|
||||
exitToken = inheritedToken;
|
||||
process.once("exit", cleanup);
|
||||
registerSignalCleanup(cleanup);
|
||||
return;
|
||||
}
|
||||
|
||||
const token = `${process.pid}-${Date.now()}-${Math.random()}`;
|
||||
const record = {
|
||||
pid: process.pid,
|
||||
startTime: processStartTime(process.pid),
|
||||
processGroup: false,
|
||||
kind,
|
||||
target,
|
||||
token,
|
||||
};
|
||||
const result = acquireProcessRecord(file, record);
|
||||
if (!result.acquired) {
|
||||
const pid = result.existing?.pid;
|
||||
throw Error(
|
||||
`Cannot run ${target}: ${describeOutputOwner(result.existing)} ` +
|
||||
`already owns ${label}${pid ? ` (pid ${pid})` : ""}.`
|
||||
);
|
||||
}
|
||||
|
||||
exitToken = token;
|
||||
process.once("exit", cleanup);
|
||||
registerSignalCleanup(cleanup);
|
||||
};
|
||||
|
||||
acquire.displayName = `lock-${label}:${target}`;
|
||||
return acquire;
|
||||
};
|
||||
|
||||
export const createWorkflowLockTask = (target) =>
|
||||
createLockTask({
|
||||
file: workflowLockFile,
|
||||
inheritedTokenEnv: WORKFLOW_LOCK_TOKEN_ENV,
|
||||
kind: "output",
|
||||
label: "build and development workflow",
|
||||
target,
|
||||
});
|
||||
@@ -23,7 +23,7 @@ export class DemoHaProgressButton extends LitElement {
|
||||
<ha-progress-button @click=${this._clickedFail}>
|
||||
Fail
|
||||
</ha-progress-button>
|
||||
<ha-progress-button size="small" @click=${this._clickedSuccess}>
|
||||
<ha-progress-button size="s" @click=${this._clickedSuccess}>
|
||||
small
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
|
||||
+12
-12
@@ -7,7 +7,7 @@
|
||||
"name": "home-assistant-frontend",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "node build-scripts/build-manager.mjs",
|
||||
"build": "script/build_frontend",
|
||||
"lint:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:prettier": "prettier . --cache --check",
|
||||
@@ -49,16 +49,16 @@
|
||||
"@codemirror/lint": "6.9.7",
|
||||
"@codemirror/search": "6.7.1",
|
||||
"@codemirror/state": "6.7.1",
|
||||
"@codemirror/view": "6.43.7",
|
||||
"@codemirror/view": "6.43.6",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.6.0",
|
||||
"@formatjs/intl-datetimeformat": "7.5.2",
|
||||
"@formatjs/intl-displaynames": "7.3.13",
|
||||
"@formatjs/intl-durationformat": "0.10.18",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.11",
|
||||
"@formatjs/intl-listformat": "8.3.13",
|
||||
"@formatjs/intl-locale": "5.3.10",
|
||||
"@formatjs/intl-numberformat": "9.4.0",
|
||||
"@formatjs/intl-numberformat": "9.3.14",
|
||||
"@formatjs/intl-pluralrules": "6.3.13",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.13",
|
||||
"@fullcalendar/core": "6.1.21",
|
||||
@@ -107,7 +107,7 @@
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "11.2.13",
|
||||
"intl-messageformat": "11.2.12",
|
||||
"js-yaml": "5.2.2",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -150,9 +150,9 @@
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.62.1",
|
||||
"@playwright/test": "1.62.0",
|
||||
"@rsdoctor/rspack-plugin": "1.6.1",
|
||||
"@rspack/core": "2.1.7",
|
||||
"@rspack/core": "2.1.5",
|
||||
"@rspack/dev-server": "2.1.0",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -186,14 +186,14 @@
|
||||
"fs-extra": "11.4.0",
|
||||
"generate-license-file": "4.2.1",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.8.0",
|
||||
"globals": "17.7.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "30.0.1",
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.2.0",
|
||||
@@ -224,12 +224,12 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"globals": "17.8.0",
|
||||
"globals": "17.7.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.18.0",
|
||||
"packageManager": "yarn@4.17.1",
|
||||
"volta": {
|
||||
"node": "24.18.1"
|
||||
"node": "24.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260729.0"
|
||||
version = "20260729.4"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -61,44 +61,10 @@ fi
|
||||
echo Core is used from ${coreUrl}
|
||||
|
||||
# build the frontend so it connects to the passed core
|
||||
HASS_URL="$coreUrl" ./node_modules/.bin/gulp develop-app &
|
||||
develop_pid=$!
|
||||
HASS_URL="$coreUrl" ./script/develop &
|
||||
|
||||
# serve the frontend
|
||||
./node_modules/.bin/serve -p $frontendPort --single --no-port-switching --config ../script/serve-config.json ./hass_frontend &
|
||||
serve_pid=$!
|
||||
|
||||
stop_children() {
|
||||
trap - EXIT INT TERM HUP
|
||||
kill "$develop_pid" "$serve_pid" 2>/dev/null || true
|
||||
wait "$develop_pid" 2>/dev/null || true
|
||||
wait "$serve_pid" 2>/dev/null || true
|
||||
}
|
||||
|
||||
trap stop_children EXIT
|
||||
trap 'stop_children; exit 130' INT
|
||||
trap 'stop_children; exit 143' TERM
|
||||
trap 'stop_children; exit 129' HUP
|
||||
|
||||
while kill -0 "$develop_pid" 2>/dev/null && kill -0 "$serve_pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
develop_status=
|
||||
serve_status=
|
||||
if ! kill -0 "$develop_pid" 2>/dev/null; then
|
||||
if wait "$develop_pid"; then develop_status=0; else develop_status=$?; fi
|
||||
fi
|
||||
if ! kill -0 "$serve_pid" 2>/dev/null; then
|
||||
if wait "$serve_pid"; then serve_status=0; else serve_status=$?; fi
|
||||
fi
|
||||
|
||||
if [ -n "$develop_status" ] && [ "$develop_status" -ne 0 ]; then
|
||||
status=$develop_status
|
||||
elif [ -n "$serve_status" ]; then
|
||||
status=$serve_status
|
||||
else
|
||||
status=$develop_status
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
# keep the script running while serving
|
||||
wait
|
||||
|
||||
@@ -18,6 +18,8 @@ export class HaProgressButton extends LitElement {
|
||||
|
||||
@property() appearance: Appearance = "accent";
|
||||
|
||||
@property() size: "xs" | "s" | "m" | "l" | "xl" = "m";
|
||||
|
||||
@property({ attribute: false }) public iconPath?: string;
|
||||
|
||||
@property() variant: "brand" | "danger" | "neutral" | "warning" | "success" =
|
||||
@@ -32,6 +34,7 @@ export class HaProgressButton extends LitElement {
|
||||
return html`
|
||||
<ha-button
|
||||
.appearance=${appearance}
|
||||
.size=${this.size}
|
||||
.disabled=${this.disabled}
|
||||
.loading=${this.progress}
|
||||
.variant=${
|
||||
@@ -118,15 +121,16 @@ export class HaProgressButton extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Fade the content out rather than hiding it, so the button keeps its
|
||||
accessible name while the result icon covers it. */
|
||||
/* The icon lives in this shadow root, so callers cannot size it themselves. */
|
||||
ha-button[size="xs"] ha-svg-icon[slot="start"],
|
||||
ha-button[size="s"] ha-svg-icon[slot="start"] {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
ha-button.result::part(start),
|
||||
ha-button.result::part(end),
|
||||
ha-button.result::part(label),
|
||||
ha-button.result::part(caret) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
ha-button.result::part(caret),
|
||||
ha-button.result::part(spinner) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { join } from "lit/directives/join";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
@@ -484,7 +483,13 @@ export class HaDataTable extends LitElement {
|
||||
: ""
|
||||
}
|
||||
${Object.entries(columns).map(([key, column]) => {
|
||||
if (!this._isColumnVisible(key, column)) {
|
||||
if (
|
||||
column.hidden ||
|
||||
(this.columnOrder && this.columnOrder.includes(key)
|
||||
? (this.hiddenColumns?.includes(key) ??
|
||||
column.defaultHidden)
|
||||
: column.defaultHidden)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
const sorted = key === this.sortColumn;
|
||||
@@ -652,7 +657,10 @@ export class HaDataTable extends LitElement {
|
||||
${Object.entries(columns).map(([key, column]) => {
|
||||
if (
|
||||
(narrow && !column.main && !column.showNarrow) ||
|
||||
!this._isColumnVisible(key, column)
|
||||
column.hidden ||
|
||||
(this.columnOrder && this.columnOrder.includes(key)
|
||||
? (this.hiddenColumns?.includes(key) ?? column.defaultHidden)
|
||||
: column.defaultHidden)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
@@ -684,19 +692,28 @@ export class HaDataTable extends LitElement {
|
||||
: narrow && column.main
|
||||
? html`<div class="primary">${row[key]}</div>
|
||||
<div class="secondary">
|
||||
${join(
|
||||
Object.entries(columns)
|
||||
.filter(([key2, column2]) =>
|
||||
this._isSecondaryColumnVisible(key2, column2)
|
||||
)
|
||||
.map(([key2, column2]) =>
|
||||
column2.template
|
||||
? column2.template(row)
|
||||
: row[key2]
|
||||
)
|
||||
.filter(this._hasCellValue),
|
||||
STRINGS_SEPARATOR_DOT
|
||||
)}
|
||||
${Object.entries(columns)
|
||||
.filter(
|
||||
([key2, column2]) =>
|
||||
!column2.hidden &&
|
||||
!column2.main &&
|
||||
!column2.showNarrow &&
|
||||
!(this.columnOrder &&
|
||||
this.columnOrder.includes(key2)
|
||||
? (this.hiddenColumns?.includes(key2) ??
|
||||
column2.defaultHidden)
|
||||
: column2.defaultHidden)
|
||||
)
|
||||
.map(
|
||||
([key2, column2], i) =>
|
||||
html`${
|
||||
i !== 0 ? STRINGS_SEPARATOR_DOT : nothing
|
||||
}${
|
||||
column2.template
|
||||
? column2.template(row)
|
||||
: row[key2]
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
${
|
||||
column.extraTemplate
|
||||
@@ -716,29 +733,6 @@ export class HaDataTable extends LitElement {
|
||||
`;
|
||||
};
|
||||
|
||||
private _isColumnVisible(key: string, column: DataTableColumnData): boolean {
|
||||
if (column.hidden) {
|
||||
return false;
|
||||
}
|
||||
if (!this.columnOrder?.includes(key)) {
|
||||
return !column.defaultHidden;
|
||||
}
|
||||
return !(this.hiddenColumns?.includes(key) ?? column.defaultHidden);
|
||||
}
|
||||
|
||||
private _isSecondaryColumnVisible(
|
||||
key: string,
|
||||
column: DataTableColumnData
|
||||
): boolean {
|
||||
if (column.main || column.showNarrow) {
|
||||
return false;
|
||||
}
|
||||
return this._isColumnVisible(key, column);
|
||||
}
|
||||
|
||||
private _hasCellValue = (value: unknown): boolean =>
|
||||
value !== undefined && value !== null && value !== "" && value !== nothing;
|
||||
|
||||
private async _sortFilterData() {
|
||||
const startTime = new Date().getTime();
|
||||
const timeBetweenUpdate = startTime - this._lastUpdate;
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
} from "../../data/media_source";
|
||||
import { isTTSMediaSource } from "../../data/tts";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { panelIsReady } from "../../layouts/panel-ready";
|
||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -162,8 +161,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
private _initialReady = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => this._attachResizeObserver());
|
||||
@@ -277,7 +274,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
ids: navigateIds,
|
||||
current: this._currentItem,
|
||||
});
|
||||
this._signalInitialReady();
|
||||
} else {
|
||||
if (!currentProm) {
|
||||
currentProm = this._fetchData(
|
||||
@@ -293,7 +289,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
ids: navigateIds,
|
||||
current: item,
|
||||
});
|
||||
this._signalInitialReady();
|
||||
},
|
||||
(err) => {
|
||||
// When we change entity ID, we will first try to see if the new entity is
|
||||
@@ -327,10 +322,8 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
),
|
||||
code: "entity_not_found",
|
||||
});
|
||||
this._signalInitialReady();
|
||||
} else {
|
||||
this._setError(err);
|
||||
this._signalInitialReady();
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1152,21 +1145,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
private _signalInitialReady(): void {
|
||||
if (this._initialReady) {
|
||||
return;
|
||||
}
|
||||
this._initialReady = true;
|
||||
const root = this.getRootNode();
|
||||
panelIsReady(
|
||||
root instanceof ShadowRoot &&
|
||||
root.host instanceof HTMLElement &&
|
||||
root.host.tagName.startsWith("HA-PANEL-")
|
||||
? root.host
|
||||
: this
|
||||
);
|
||||
}
|
||||
|
||||
private _setError(error: any) {
|
||||
if (!this.dialog) {
|
||||
this._error = error;
|
||||
@@ -1231,8 +1209,8 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
private _animateHeaderHeight() {
|
||||
let start: number | undefined;
|
||||
const animate = (time: number) => {
|
||||
let start;
|
||||
const animate = (time) => {
|
||||
if (start === undefined) {
|
||||
start = time;
|
||||
}
|
||||
|
||||
+14
-2
@@ -793,6 +793,17 @@ const findEnergyDataCollection = (
|
||||
return (hass.connection as any)[key];
|
||||
};
|
||||
|
||||
// The last-picked preset is remembered per energy collection, so each dashboard
|
||||
// reopens on its own default period. Derived from the connection key so the read
|
||||
// and write sides cannot drift apart.
|
||||
export const getEnergyDefaultPeriodStorageKey = (
|
||||
hass: HomeAssistant,
|
||||
collectionKey?: string
|
||||
): string => {
|
||||
const [key] = convertCollectionKeyToConnection(hass, collectionKey);
|
||||
return `energy-default-period-${key}`;
|
||||
};
|
||||
|
||||
// When does the collection's day period need to roll over to the next day?
|
||||
// With `midnightRollover` (the real-time "Now" view) it rolls over right at
|
||||
// midnight. Otherwise it waits an hour, until the new day's first hourly
|
||||
@@ -892,8 +903,9 @@ export const getEnergyDataCollection = (
|
||||
// The real-time "Now" view always tracks today; it shows live data even
|
||||
// before today's first statistic exists, so it never falls back to yesterday.
|
||||
const preferredPeriod =
|
||||
(localStorage.getItem(`energy-default-period-${key}`) as DateRange) ||
|
||||
"today";
|
||||
(localStorage.getItem(
|
||||
getEnergyDefaultPeriodStorageKey(hass, options.key)
|
||||
) as DateRange) || "today";
|
||||
const period =
|
||||
preferredPeriod === "today" && hour === "0" && !midnightRollover
|
||||
? "yesterday"
|
||||
|
||||
@@ -23,7 +23,6 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
badges?: HuiBadge[];
|
||||
sections?: HuiSection[];
|
||||
isStrategy: boolean;
|
||||
initialRenderComplete?: Promise<void>;
|
||||
setConfig(config: LovelaceViewConfig): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -314,11 +314,6 @@ export interface ZWaveJSSetConfigParamResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ZwaveJSNodeConfigParameterUpdate {
|
||||
id: string;
|
||||
value: number | null;
|
||||
}
|
||||
|
||||
export interface ZWaveJSDataCollectionStatus {
|
||||
enabled: boolean;
|
||||
opted_in: boolean;
|
||||
@@ -737,16 +732,6 @@ export const fetchZwaveNodeConfigParameters = (
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const subscribeZwaveNodeConfigParameterUpdates = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
callback: (update: ZwaveJSNodeConfigParameterUpdate) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "zwave_js/subscribe_config_parameter_updates",
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const setZwaveNodeConfigParameter = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -12,13 +12,11 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog, ShowDialogParams } from "../make-dialog-manager";
|
||||
import type { FormDialogData, FormDialogParams } from "./show-form-dialog";
|
||||
import type { HaForm } from "../../components/ha-form/ha-form";
|
||||
|
||||
interface StackEntry {
|
||||
params: FormDialogParams;
|
||||
data: FormDialogData;
|
||||
nestedField?: string;
|
||||
error?: Record<string, string>;
|
||||
}
|
||||
|
||||
@customElement("dialog-form")
|
||||
@@ -38,15 +36,10 @@ export class DialogForm
|
||||
|
||||
@state() private _stack: StackEntry[] = [];
|
||||
|
||||
@state() private _error?: Record<string, string>;
|
||||
|
||||
@query("ha-form") private _form?: HaForm;
|
||||
|
||||
public async showDialog(params: FormDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._data = params.data || {};
|
||||
this._open = true;
|
||||
this._error = undefined;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
}
|
||||
|
||||
@@ -66,17 +59,11 @@ export class DialogForm
|
||||
const origin = ev.composedPath()[0] as HTMLElement & { name?: string };
|
||||
this._stack = [
|
||||
...this._stack,
|
||||
{
|
||||
params: this._params!,
|
||||
data: this._data,
|
||||
nestedField: origin?.name,
|
||||
error: this._error,
|
||||
},
|
||||
{ params: this._params!, data: this._data, nestedField: origin?.name },
|
||||
];
|
||||
const nested = ev.detail.dialogParams as FormDialogParams;
|
||||
this._params = nested;
|
||||
this._data = nested?.data || {};
|
||||
this._error = undefined;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
};
|
||||
|
||||
@@ -88,7 +75,6 @@ export class DialogForm
|
||||
this._stack = this._stack.slice(0, -1);
|
||||
this._params = prev.params;
|
||||
this._data = prev.data;
|
||||
this._error = prev.error;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
return prev.nestedField;
|
||||
}
|
||||
@@ -102,18 +88,10 @@ export class DialogForm
|
||||
this._params = undefined;
|
||||
this._data = {};
|
||||
this._open = false;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _submit(): void {
|
||||
if (this._form && !this._form.reportValidity()) {
|
||||
this._error = {
|
||||
base: this.hass!.localize("ui.components.form.validation_failed"),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this._closeState = "submitted";
|
||||
const submit = this._params?.submit;
|
||||
const data = this._data;
|
||||
@@ -141,7 +119,6 @@ export class DialogForm
|
||||
: data;
|
||||
|
||||
this._data = deepClone({ ...this._data, [nestedField]: newValue });
|
||||
this._error = undefined;
|
||||
this._updateDirtyState(this._data);
|
||||
}
|
||||
|
||||
@@ -159,7 +136,6 @@ export class DialogForm
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
this._data = ev.detail.value;
|
||||
this._error = undefined;
|
||||
this._updateDirtyState(this._data);
|
||||
}
|
||||
|
||||
@@ -182,7 +158,6 @@ export class DialogForm
|
||||
.computeHelper=${this._params.computeHelper}
|
||||
.data=${this._data}
|
||||
.schema=${this._params.schema}
|
||||
.error=${this._error}
|
||||
@value-changed=${this._valueChanged}
|
||||
@show-dialog=${this._handleNestedShowDialog}
|
||||
>
|
||||
|
||||
+1
-12
@@ -37,26 +37,15 @@ declare global {
|
||||
}
|
||||
|
||||
const clearUrlParams = () => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
let changed = false;
|
||||
// Clear auth data from url if we have been able to establish a connection
|
||||
if (location.search.includes("auth_callback=1")) {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
|
||||
// Remove all data from QueryCallbackData type
|
||||
searchParams.delete("auth_callback");
|
||||
searchParams.delete("code");
|
||||
searchParams.delete("state");
|
||||
searchParams.delete("storeToken");
|
||||
changed = true;
|
||||
}
|
||||
// Remove the cache-busting param added by the stale-index recovery guard in
|
||||
// index.html once we have booted successfully, so it doesn't linger in the
|
||||
// URL (and stops acting as the guard's one-shot loop marker).
|
||||
if (searchParams.has("ha_cache_bust")) {
|
||||
searchParams.delete("ha_cache_bust");
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
const search = searchParams.toString();
|
||||
history.replaceState(
|
||||
null,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { CSSResult } from "lit";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
@@ -7,6 +8,7 @@ import { navigate } from "../common/navigate";
|
||||
import type { CustomPanelInfo } from "../data/panel_custom";
|
||||
import { baseEntrypointStyles } from "../resources/styles";
|
||||
import { createCustomPanelElement } from "../util/custom-panel/create-custom-panel-element";
|
||||
import { dropRealmCollections } from "../util/custom-panel/drop-realm-collections";
|
||||
import { loadCustomPanel } from "../util/custom-panel/load-custom-panel";
|
||||
import { setCustomPanelProperties } from "../util/custom-panel/set-custom-panel-properties";
|
||||
|
||||
@@ -29,8 +31,14 @@ window.loadES5Adapter = () => {
|
||||
|
||||
let panelEl: HTMLElement | undefined;
|
||||
let initialized = false;
|
||||
// Kept so we can clean up after ourselves on pagehide without depending on
|
||||
// `window.parent.customPanel`, which `_cleanupPanel()` may already have deleted.
|
||||
let connection: Connection | undefined;
|
||||
|
||||
function setProperties(properties) {
|
||||
if (properties.hass?.connection) {
|
||||
connection = properties.hass.connection;
|
||||
}
|
||||
if (!panelEl) {
|
||||
return;
|
||||
}
|
||||
@@ -150,4 +158,9 @@ window.addEventListener("pagehide", () => {
|
||||
while (document.body.lastChild) {
|
||||
document.body.removeChild(document.body.lastChild);
|
||||
}
|
||||
// The connection is owned by the main window and outlives this realm, so any
|
||||
// collection we created on it has to go with us.
|
||||
if (connection) {
|
||||
dropRealmCollections(connection);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
<script>
|
||||
/*
|
||||
* Boot-time recovery from a stale index.html.
|
||||
*
|
||||
* index.html pins the content-hashed entry bundles (core.<hash>.js /
|
||||
* app.<hash>.js). After a frontend release those files are deleted from
|
||||
* disk, so a cached (stale) index.html imports URLs that now 404 and the
|
||||
* app never boots - the launch screen stays up forever. No bundled JS can
|
||||
* recover this, because the bundle itself failed to load, so this guard has
|
||||
* to live inline in the HTML document.
|
||||
*
|
||||
* On a failed entry load we navigate once to the same URL with a
|
||||
* cache-busting query param (and, on https, after dropping the service
|
||||
* worker + its caches) so the browser fetches a fresh index.html that points
|
||||
* at the current hashes. The param doubles as a one-shot loop guard: if the
|
||||
* freshly fetched index still fails we stop and show a message instead of
|
||||
* reloading forever. core.ts strips the param again after a successful boot.
|
||||
*/
|
||||
(function () {
|
||||
var BUST_PARAM = "ha_cache_bust";
|
||||
// Only production entry bundles carry a content hash. Requiring the hash
|
||||
// keeps the guard inert in development (unhashed core.js / app.js) and
|
||||
// scoped to the entry chunks whose 404 is fatal to boot - not translations
|
||||
// or lazily loaded panels, which fail non-fatally and are handled elsewhere.
|
||||
// Injected from stale-build-patterns.json (the single source shared with
|
||||
// the bundled util/recover-stale-build.ts) so the two never drift.
|
||||
var HASHED_ENTRY = new RegExp(<%= JSON.stringify(staleBuildPatterns.hashedEntry) %>, "i");
|
||||
var MODULE_ERROR = new RegExp(<%= JSON.stringify(staleBuildPatterns.moduleError) %>, "i");
|
||||
|
||||
function booted() {
|
||||
// The launch screen is only removed once the app has taken over the
|
||||
// page. If it is gone, any later chunk failure is post-boot (a lazy
|
||||
// panel/dialog) and is not the stale-index boot failure this guard
|
||||
// targets, so we leave it alone.
|
||||
return !document.getElementById("ha-launch-screen");
|
||||
}
|
||||
|
||||
function showFatal() {
|
||||
var box = document.getElementById("ha-launch-screen-info-box");
|
||||
if (box) {
|
||||
box.textContent =
|
||||
"Could not load Home Assistant. Please refresh the page, and clear your browser cache if the problem persists.";
|
||||
}
|
||||
}
|
||||
|
||||
function recover() {
|
||||
if (location.search.indexOf(BUST_PARAM + "=") !== -1) {
|
||||
// We already reloaded once with a fresh index and it still failed;
|
||||
// stop to avoid a reload loop.
|
||||
showFatal();
|
||||
return;
|
||||
}
|
||||
var target =
|
||||
location.pathname +
|
||||
location.search +
|
||||
(location.search ? "&" : "?") +
|
||||
BUST_PARAM +
|
||||
"=" +
|
||||
Date.now() +
|
||||
location.hash;
|
||||
// On https the service worker "/" route ignores the query string
|
||||
// (ignoreSearch), so a bust param alone would still be answered with the
|
||||
// stale cached index. Drop the worker and its caches first so the reload
|
||||
// is served fresh from the network.
|
||||
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
|
||||
var go = function () {
|
||||
location.replace(target);
|
||||
};
|
||||
Promise.all([
|
||||
navigator.serviceWorker
|
||||
.getRegistrations()
|
||||
.then(function (regs) {
|
||||
return Promise.all(
|
||||
regs.map(function (r) {
|
||||
return r.unregister();
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(function () {}),
|
||||
self.caches
|
||||
? caches
|
||||
.keys()
|
||||
.then(function (keys) {
|
||||
return Promise.all(
|
||||
keys.map(function (k) {
|
||||
return caches.delete(k);
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(function () {})
|
||||
: null,
|
||||
]).then(go, go);
|
||||
} else {
|
||||
location.replace(target);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRecover(url, message) {
|
||||
if (booted()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(url && HASHED_ENTRY.test(url)) ||
|
||||
(message && (HASHED_ENTRY.test(message) || MODULE_ERROR.test(message)))
|
||||
) {
|
||||
recover();
|
||||
}
|
||||
}
|
||||
|
||||
// Resource-load errors (<script>, modulepreload <link>) do not bubble, so
|
||||
// they are only observable in the capture phase at the window.
|
||||
window.addEventListener(
|
||||
"error",
|
||||
function (ev) {
|
||||
var el = ev.target;
|
||||
if (el && (el.tagName === "SCRIPT" || el.tagName === "LINK")) {
|
||||
maybeRecover(el.href || el.src, null);
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// A failed dynamic import() rejects; the inline entry imports do not catch
|
||||
// it, so the rejection surfaces here.
|
||||
window.addEventListener("unhandledrejection", function (ev) {
|
||||
var reason = ev && ev.reason;
|
||||
maybeRecover(null, reason && (reason.message || String(reason)));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -2,7 +2,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Home Assistant</title>
|
||||
<% if (useCacheRecovery) { %><%= renderTemplate("_bootstrap_recovery.html.template") %><% } %>
|
||||
<%= renderTemplate("_header.html.template") %>
|
||||
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#18bcf2" />
|
||||
<link
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../components/animation/ha-fade-in";
|
||||
import "../components/ha-top-app-bar-fixed";
|
||||
import "../components/ha-spinner";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -37,9 +36,7 @@ class HassLoadingScreen extends LitElement {
|
||||
private _renderContent(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner></ha-spinner>
|
||||
</ha-fade-in>
|
||||
<ha-spinner></ha-spinner>
|
||||
${
|
||||
this.message
|
||||
? html`<div id="loading-text">${this.message}</div>`
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ContextProvider, createContext } from "@lit/context";
|
||||
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@@ -17,61 +15,6 @@ export const panelIsReady = async (element: HTMLElement) => {
|
||||
fireEvent(element, "hass-panel-ready");
|
||||
};
|
||||
|
||||
export type RegisterChildPanelReady = (ready: Promise<void>) => void;
|
||||
|
||||
export const childPanelReadyContext =
|
||||
createContext<RegisterChildPanelReady>("child-panel-ready");
|
||||
|
||||
export class ChildPanelReady implements ReactiveController {
|
||||
private _promises: Promise<void>[] = [];
|
||||
|
||||
private _host: ReactiveControllerHost & HTMLElement;
|
||||
|
||||
private _resolveReady?: () => void;
|
||||
|
||||
private _completing = false;
|
||||
|
||||
public ready = new Promise<void>((resolve) => {
|
||||
this._resolveReady = resolve;
|
||||
});
|
||||
|
||||
public constructor(host: ReactiveControllerHost & HTMLElement) {
|
||||
this._host = host;
|
||||
host.addController(this);
|
||||
new ContextProvider(host, {
|
||||
context: childPanelReadyContext,
|
||||
initialValue: (ready) => this._promises.push(ready),
|
||||
});
|
||||
}
|
||||
|
||||
public hostUpdated() {
|
||||
if (this._completing) {
|
||||
return;
|
||||
}
|
||||
this._completing = true;
|
||||
this._host.removeController(this);
|
||||
void this._complete();
|
||||
}
|
||||
|
||||
private async _complete() {
|
||||
// Children created/updated during this render register after the host's
|
||||
// update commits. Wait for that before snapshotting readiness promises.
|
||||
await this._host.updateComplete;
|
||||
|
||||
await this._waitForPromises();
|
||||
|
||||
this._resolveReady?.();
|
||||
await panelIsReady(this._host);
|
||||
}
|
||||
|
||||
private _waitForPromises(): Promise<void> {
|
||||
const count = this._promises.length;
|
||||
return Promise.allSettled(this._promises).then(() =>
|
||||
this._promises.length > count ? this._waitForPromises() : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PanelReady {
|
||||
public ready?: Promise<void>;
|
||||
|
||||
|
||||
@@ -19,66 +19,29 @@ import { HassRouterPage } from "./hass-router-page";
|
||||
|
||||
const CACHE_URL_PATHS = ["lovelace", "home", "config"];
|
||||
const PANEL_READY_TIMEOUT = 2000;
|
||||
const DASHBOARD_READY_TIMEOUT = 5000;
|
||||
const COMPONENTS = {
|
||||
app: { load: () => import("../panels/app/ha-panel-app") },
|
||||
energy: {
|
||||
load: () => import("../panels/energy/ha-panel-energy"),
|
||||
waitForReady: true,
|
||||
readyTimeout: DASHBOARD_READY_TIMEOUT,
|
||||
},
|
||||
calendar: {
|
||||
load: () => import("../panels/calendar/ha-panel-calendar"),
|
||||
waitForReady: true,
|
||||
},
|
||||
config: { load: () => import("../panels/config/ha-panel-config") },
|
||||
custom: { load: () => import("../panels/custom/ha-panel-custom") },
|
||||
lovelace: {
|
||||
load: () => import("../panels/lovelace/ha-panel-lovelace"),
|
||||
waitForReady: true,
|
||||
readyTimeout: DASHBOARD_READY_TIMEOUT,
|
||||
},
|
||||
history: { load: () => import("../panels/history/ha-panel-history") },
|
||||
iframe: { load: () => import("../panels/iframe/ha-panel-iframe") },
|
||||
logbook: { load: () => import("../panels/logbook/ha-panel-logbook") },
|
||||
map: { load: () => import("../panels/map/ha-panel-map") },
|
||||
my: { load: () => import("../panels/my/ha-panel-my") },
|
||||
profile: { load: () => import("../panels/profile/ha-panel-profile") },
|
||||
todo: { load: () => import("../panels/todo/ha-panel-todo") },
|
||||
"media-browser": {
|
||||
load: () => import("../panels/media-browser/ha-panel-media-browser"),
|
||||
waitForReady: true,
|
||||
},
|
||||
light: {
|
||||
load: () => import("../panels/light/ha-panel-light"),
|
||||
waitForReady: true,
|
||||
readyTimeout: DASHBOARD_READY_TIMEOUT,
|
||||
},
|
||||
security: {
|
||||
load: () => import("../panels/security/ha-panel-security"),
|
||||
waitForReady: true,
|
||||
readyTimeout: DASHBOARD_READY_TIMEOUT,
|
||||
},
|
||||
climate: {
|
||||
load: () => import("../panels/climate/ha-panel-climate"),
|
||||
waitForReady: true,
|
||||
readyTimeout: DASHBOARD_READY_TIMEOUT,
|
||||
},
|
||||
maintenance: {
|
||||
load: () => import("../panels/maintenance/ha-panel-maintenance"),
|
||||
waitForReady: true,
|
||||
readyTimeout: DASHBOARD_READY_TIMEOUT,
|
||||
},
|
||||
home: {
|
||||
load: () => import("../panels/home/ha-panel-home"),
|
||||
waitForReady: true,
|
||||
readyTimeout: DASHBOARD_READY_TIMEOUT,
|
||||
},
|
||||
notfound: { load: () => import("../panels/notfound/ha-panel-notfound") },
|
||||
} satisfies Record<
|
||||
string,
|
||||
Pick<RouteOptions, "load" | "waitForReady"> & { readyTimeout?: number }
|
||||
>;
|
||||
app: () => import("../panels/app/ha-panel-app"),
|
||||
energy: () => import("../panels/energy/ha-panel-energy"),
|
||||
calendar: () => import("../panels/calendar/ha-panel-calendar"),
|
||||
config: () => import("../panels/config/ha-panel-config"),
|
||||
custom: () => import("../panels/custom/ha-panel-custom"),
|
||||
lovelace: () => import("../panels/lovelace/ha-panel-lovelace"),
|
||||
history: () => import("../panels/history/ha-panel-history"),
|
||||
iframe: () => import("../panels/iframe/ha-panel-iframe"),
|
||||
logbook: () => import("../panels/logbook/ha-panel-logbook"),
|
||||
map: () => import("../panels/map/ha-panel-map"),
|
||||
my: () => import("../panels/my/ha-panel-my"),
|
||||
profile: () => import("../panels/profile/ha-panel-profile"),
|
||||
todo: () => import("../panels/todo/ha-panel-todo"),
|
||||
"media-browser": () =>
|
||||
import("../panels/media-browser/ha-panel-media-browser"),
|
||||
light: () => import("../panels/light/ha-panel-light"),
|
||||
security: () => import("../panels/security/ha-panel-security"),
|
||||
climate: () => import("../panels/climate/ha-panel-climate"),
|
||||
maintenance: () => import("../panels/maintenance/ha-panel-maintenance"),
|
||||
home: () => import("../panels/home/ha-panel-home"),
|
||||
notfound: () => import("../panels/notfound/ha-panel-notfound"),
|
||||
};
|
||||
|
||||
@customElement("partial-panel-resolver")
|
||||
class PartialPanelResolver extends HassRouterPage {
|
||||
@@ -163,12 +126,13 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
private _getRoutes(panels: Panels): RouterOptions {
|
||||
const routes: RouterOptions["routes"] = {};
|
||||
Object.values(panels).forEach((panel) => {
|
||||
const component = COMPONENTS[panel.component_name];
|
||||
const data: RouteOptions = {
|
||||
tag: `ha-panel-${panel.component_name}`,
|
||||
cache: CACHE_URL_PATHS.includes(panel.url_path),
|
||||
...(component ?? {}),
|
||||
};
|
||||
if (panel.component_name in COMPONENTS) {
|
||||
data.load = COMPONENTS[panel.component_name];
|
||||
}
|
||||
routes[panel.url_path] = data;
|
||||
});
|
||||
|
||||
@@ -257,12 +221,9 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
)
|
||||
) {
|
||||
await this.rebuild();
|
||||
const component =
|
||||
COMPONENTS[this.hass.panels[this._currentPage].component_name];
|
||||
await promiseTimeout(
|
||||
component?.readyTimeout ?? PANEL_READY_TIMEOUT,
|
||||
this.pageRendered
|
||||
).catch(() => undefined);
|
||||
await promiseTimeout(PANEL_READY_TIMEOUT, this.pageRendered).catch(
|
||||
() => undefined
|
||||
);
|
||||
// Only fire frontend/loaded when this call actually removed the launch
|
||||
// screen, so later panel updates do not fire it again. Native apps remove
|
||||
// it instantly because their own splash screen is still visible.
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomCurrentTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../components/radio/ha-radio-group";
|
||||
import "../../components/radio/ha-radio-option";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-dialog";
|
||||
import { RecurrenceRange } from "../../data/calendar";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ConfirmEventDialogBoxParams } from "./show-confirm-event-dialog-box";
|
||||
|
||||
// `RecurrenceRange.THISEVENT` is "", which the radio group treats as unselected
|
||||
// on its value-attribute and form-reset paths, so keep the options on their own
|
||||
// literals and map them when confirming.
|
||||
type RecurrenceScope = "this" | "future";
|
||||
import "../../components/ha-button";
|
||||
|
||||
@customElement("confirm-event-dialog-box")
|
||||
class ConfirmEventDialogBox extends LitElement {
|
||||
@@ -27,14 +18,11 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _closeState?: "canceled" | "confirmed";
|
||||
|
||||
@state() private _scope: RecurrenceScope = "this";
|
||||
@state()
|
||||
private _closeState?: "canceled" | "confirmed" | "confirmedFuture";
|
||||
|
||||
public async showDialog(params: ConfirmEventDialogBoxParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._scope = "this";
|
||||
this._closeState = undefined;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -51,27 +39,20 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { destructive, recurring } = this._params;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
width="small"
|
||||
type="alert"
|
||||
aria-describedby=${recurring ? nothing : "description"}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${
|
||||
recurring
|
||||
? this._renderRecurrenceRange()
|
||||
: html`<p id="description">${this._params.text}</p>`
|
||||
}
|
||||
<div>
|
||||
<p>${this._params.text}</p>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._dismiss}
|
||||
?autofocus=${!recurring && destructive}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
@@ -79,41 +60,29 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._confirm}
|
||||
?autofocus=${!recurring && !destructive}
|
||||
variant=${destructive ? "danger" : "brand"}
|
||||
autofocus
|
||||
variant="danger"
|
||||
>
|
||||
${this._params.confirmText}
|
||||
</ha-button>
|
||||
${
|
||||
this._params.confirmFutureText
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._confirmFuture}
|
||||
slot="primaryAction"
|
||||
variant="danger"
|
||||
>
|
||||
${this._params.confirmFutureText}
|
||||
</ha-button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRecurrenceRange() {
|
||||
const thisEvent = this.hass.localize(
|
||||
"ui.components.calendar.event.recurrence_range.this_event"
|
||||
);
|
||||
const thisAndFuture = this.hass.localize(
|
||||
"ui.components.calendar.event.recurrence_range.this_and_future"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-radio-group
|
||||
name="recurrence_range"
|
||||
.label=${this._params!.text ?? this._params!.title}
|
||||
.value=${this._scope}
|
||||
@change=${this._scopeChanged}
|
||||
>
|
||||
<ha-radio-option value="this" autofocus>${thisEvent}</ha-radio-option>
|
||||
<ha-radio-option value="future">${thisAndFuture}</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
`;
|
||||
}
|
||||
|
||||
private _scopeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>): void {
|
||||
this._scope = ev.currentTarget.value as RecurrenceScope;
|
||||
}
|
||||
|
||||
private _dismiss(): void {
|
||||
this._closeState = "canceled";
|
||||
this.closeDialog();
|
||||
@@ -121,11 +90,17 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
|
||||
private _confirm(): void {
|
||||
this._closeState = "confirmed";
|
||||
this._params!.confirm?.(
|
||||
this._scope === "future"
|
||||
? RecurrenceRange.THISANDFUTURE
|
||||
: RecurrenceRange.THISEVENT
|
||||
);
|
||||
if (this._params!.confirm) {
|
||||
this._params!.confirm(RecurrenceRange.THISEVENT);
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _confirmFuture(): void {
|
||||
this._closeState = "confirmedFuture";
|
||||
if (this._params!.confirm) {
|
||||
this._params!.confirm(RecurrenceRange.THISANDFUTURE);
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -133,7 +108,10 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
if (this._closeState !== "confirmed") {
|
||||
if (
|
||||
this._closeState !== "confirmed" &&
|
||||
this._closeState !== "confirmedFuture"
|
||||
) {
|
||||
this._params.cancel?.();
|
||||
}
|
||||
this._params = undefined;
|
||||
@@ -147,12 +125,18 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
pointer-events: initial !important;
|
||||
cursor: initial !important;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-radio-group::part(form-control-label) {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
.no-bottom-padding {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-dialog {
|
||||
/* Place above other dialogs */
|
||||
|
||||
@@ -224,9 +224,18 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.prompt"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
recurring: !!entry.recurrence_id,
|
||||
destructive: true,
|
||||
confirmText: entry.recurrence_id
|
||||
? this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete_this"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete"
|
||||
),
|
||||
confirmFutureText: entry.recurrence_id
|
||||
? this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete_future"
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
if (range === undefined) {
|
||||
// Cancel
|
||||
|
||||
@@ -569,8 +569,12 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
|
||||
text: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_update.recurring_prompt"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.update"),
|
||||
recurring: true,
|
||||
confirmText: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_update.update_this"
|
||||
),
|
||||
confirmFutureText: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_update.update_future"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (range === undefined) {
|
||||
@@ -621,9 +625,18 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
|
||||
: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.prompt"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
recurring: !!entry.recurrence_id,
|
||||
destructive: true,
|
||||
confirmText: entry.recurrence_id
|
||||
? this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete_this"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete"
|
||||
),
|
||||
confirmFutureText: entry.recurrence_id
|
||||
? this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete_future"
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
if (range === undefined) {
|
||||
// Cancel
|
||||
|
||||
@@ -35,7 +35,6 @@ import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import { subscribeEntityRegistry } from "../../data/entity/entity_registry";
|
||||
import { fetchIntegrationManifest } from "../../data/integration";
|
||||
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { panelIsReady } from "../../layouts/panel-ready";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { CalendarViewChanged, HomeAssistant } from "../../types";
|
||||
@@ -78,8 +77,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _mql?: MediaQueryList;
|
||||
|
||||
private _initialReady = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._mql = window.matchMedia(
|
||||
@@ -106,10 +103,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
this._entityRegistry = entities;
|
||||
// Refresh calendars when entity registry updates (includes color changes)
|
||||
this._calendars = getCalendars(this.hass, this, this._entityRegistry);
|
||||
if (!this._initialReady) {
|
||||
this._initialReady = true;
|
||||
panelIsReady(this);
|
||||
}
|
||||
// Resubscribe events if view dates are available (handles both initial load and color updates)
|
||||
if (this._start && this._end) {
|
||||
this._unsubscribeAll().then(() => {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { RecurrenceRange } from "../../data/calendar";
|
||||
|
||||
export interface ConfirmEventDialogBoxParams {
|
||||
title: string;
|
||||
// Labels the recurrence range options when `recurring` is set
|
||||
text?: string;
|
||||
confirmText?: string;
|
||||
// Let the user pick which occurrences of a recurring event are affected
|
||||
recurring?: boolean;
|
||||
destructive?: boolean;
|
||||
confirmFutureText?: string; // Prompt for future recurring events
|
||||
confirm?: (recurrenceRange: RecurrenceRange) => void;
|
||||
cancel?: () => void;
|
||||
text?: string | TemplateResult;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const loadGenericDialog = () => import("./confirm-event-dialog-box");
|
||||
|
||||
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { ChildPanelReady } from "../../layouts/panel-ready";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
@@ -32,8 +31,6 @@ class PanelClimate extends LitElement {
|
||||
|
||||
@state() private _searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
private _childPanelReady?: ChildPanelReady;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
@@ -130,7 +127,6 @@ class PanelClimate extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._childPanelReady ??= new ChildPanelReady(this);
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
||||
@@ -456,7 +455,7 @@ class DialogAreaDetail
|
||||
return deviceReg && deviceReg.area_id === areaId;
|
||||
};
|
||||
|
||||
private _nameChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
this._updateDirtyState(this._currentState());
|
||||
@@ -480,9 +479,7 @@ class DialogAreaDetail
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _pictureChanged(
|
||||
ev: ValueChangedEvent<string | null> & HASSDomTargetEvent<HaPictureUpload>
|
||||
) {
|
||||
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
|
||||
this._error = undefined;
|
||||
this._picture = (ev.target as HaPictureUpload).value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
@@ -493,9 +490,7 @@ class DialogAreaDetail
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _sensorChanged(
|
||||
ev: ValueChangedEvent<string> & HASSDomTargetEvent<HaEntityPicker>
|
||||
): void {
|
||||
private _sensorChanged(ev: CustomEvent): void {
|
||||
const deviceClass = (ev.target as HaEntityPicker).includeDeviceClasses![0];
|
||||
const key = `_${deviceClass}Entity`;
|
||||
this[key] = ev.detail.value || null;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/chips/ha-chip-set";
|
||||
import "../../../components/chips/ha-input-chip";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -337,13 +336,13 @@ class DialogFloorDetail extends DirtyStateProviderMixin<FloorFormState>()(
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _levelChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
|
||||
private _levelChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._level =
|
||||
(ev.target as HaInput).value === ""
|
||||
|
||||
@@ -346,19 +346,17 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
maxWidth: "82px",
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
type: "overflow",
|
||||
title: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
template: (automation) =>
|
||||
narrow
|
||||
? automation.formatted_state
|
||||
: html`
|
||||
<ha-switch
|
||||
@click=${stopPropagation}
|
||||
@change=${this._handleSwitchToggle}
|
||||
.automation=${automation}
|
||||
.checked=${automation.state === "on"}
|
||||
></ha-switch>
|
||||
`,
|
||||
template: (automation) => html`
|
||||
<ha-switch
|
||||
@click=${stopPropagation}
|
||||
@change=${this._handleSwitchToggle}
|
||||
.automation=${automation}
|
||||
.checked=${automation.state === "on"}
|
||||
></ha-switch>
|
||||
`,
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { mdiClose, mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { withViewTransition } from "../../../common/util/view-transition";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
@@ -280,7 +279,7 @@ class DialogImportBlueprint extends DirtyStateProviderMixin<BlueprintImportState
|
||||
});
|
||||
}
|
||||
|
||||
private _inputChanged(ev: HASSDomTargetEvent<HaInput>) {
|
||||
private _inputChanged(ev: Event) {
|
||||
this._updateDirtyState({
|
||||
value: (ev.target as HaInput).value ?? "",
|
||||
hasResult: !!this._result,
|
||||
|
||||
@@ -13,10 +13,7 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@@ -486,7 +483,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
this._createNew(blueprint);
|
||||
}
|
||||
|
||||
private _handleUsageClick = (ev: HASSDomCurrentTargetEvent<HTMLElement>) => {
|
||||
private _handleUsageClick = (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const target = ev.currentTarget as HTMLElement | null;
|
||||
|
||||
@@ -203,10 +203,12 @@ export function getModifiedAtTableColumn<T>(
|
||||
}
|
||||
|
||||
const renderDateTimeColumn = (valueDateTime: number, hass: HomeAssistant) =>
|
||||
valueDateTime
|
||||
? formatShortDateTimeWithConditionalYear(
|
||||
new Date(valueDateTime * 1000),
|
||||
hass.locale,
|
||||
hass.config
|
||||
)
|
||||
: nothing;
|
||||
html`${
|
||||
valueDateTime
|
||||
? formatShortDateTimeWithConditionalYear(
|
||||
new Date(valueDateTime * 1000),
|
||||
hass.locale,
|
||||
hass.config
|
||||
)
|
||||
: nothing
|
||||
}`;
|
||||
|
||||
@@ -36,7 +36,6 @@ import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import { ChildPanelReady } from "../../../layouts/panel-ready";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -158,11 +157,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
total: 0,
|
||||
};
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
new ChildPanelReady(this);
|
||||
}
|
||||
|
||||
private _pages = memoizeOne(
|
||||
(
|
||||
cloudStatus,
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { filterNavigationPages } from "../../../common/config/filter_navigation_pages";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import {
|
||||
childPanelReadyContext,
|
||||
type RegisterChildPanelReady,
|
||||
} from "../../../layouts/panel-ready";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-config-navigation-list";
|
||||
|
||||
@@ -24,53 +18,21 @@ class HaConfigNavigation extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public pages!: PageNavigation[];
|
||||
|
||||
@state() private _visiblePages?: PageNavigation[];
|
||||
@state() private _hasBluetoothConfigEntries = false;
|
||||
|
||||
private _hasBluetoothConfigEntries = false;
|
||||
|
||||
private _bluetoothEntriesLoaded?: Promise<void>;
|
||||
|
||||
private _childReadyRegistered = false;
|
||||
|
||||
private _filterNavigationPages = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
pages: PageNavigation[],
|
||||
hasBluetoothConfigEntries: boolean
|
||||
) => filterNavigationPages(hass, pages, { hasBluetoothConfigEntries })
|
||||
);
|
||||
|
||||
@consume({ context: childPanelReadyContext, subscribe: true })
|
||||
private _registerChildPanelReady?: RegisterChildPanelReady;
|
||||
|
||||
protected override updated(changedProps: Map<PropertyKey, unknown>) {
|
||||
super.updated(changedProps);
|
||||
|
||||
const pagesOrHassChanged =
|
||||
changedProps.has("pages") || changedProps.has("hass");
|
||||
|
||||
if (pagesOrHassChanged) {
|
||||
const ready = this._resolveVisiblePages();
|
||||
if (!this._childReadyRegistered && this._registerChildPanelReady) {
|
||||
this._registerChildPanelReady(ready);
|
||||
this._childReadyRegistered = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._childReadyRegistered &&
|
||||
this._registerChildPanelReady &&
|
||||
this.pages &&
|
||||
this.hass
|
||||
) {
|
||||
this._registerChildPanelReady(this._resolveVisiblePages());
|
||||
this._childReadyRegistered = true;
|
||||
}
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
}).then((bluetoothEntries) => {
|
||||
this._hasBluetoothConfigEntries = bluetoothEntries.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const pages = (this._visiblePages ?? []).map((page) => ({
|
||||
const pages = filterNavigationPages(this.hass, this.pages, {
|
||||
hasBluetoothConfigEntries: this._hasBluetoothConfigEntries,
|
||||
}).map((page) => ({
|
||||
...page,
|
||||
name:
|
||||
page.name ||
|
||||
@@ -113,42 +75,6 @@ class HaConfigNavigation extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _loadBluetoothEntries(): Promise<void> {
|
||||
if (!this._bluetoothEntriesLoaded) {
|
||||
this._bluetoothEntriesLoaded = getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
})
|
||||
.then((entries) => {
|
||||
this._hasBluetoothConfigEntries = entries.length > 0;
|
||||
})
|
||||
.catch(() => {
|
||||
this._hasBluetoothConfigEntries = false;
|
||||
});
|
||||
}
|
||||
return this._bluetoothEntriesLoaded;
|
||||
}
|
||||
|
||||
private async _resolveVisiblePages(): Promise<void> {
|
||||
if (this.pages.some((page) => page.component === "bluetooth")) {
|
||||
await this._loadBluetoothEntries();
|
||||
}
|
||||
|
||||
const visiblePages = this._filterNavigationPages(
|
||||
this.hass,
|
||||
this.pages,
|
||||
this._hasBluetoothConfigEntries
|
||||
);
|
||||
const currentVisiblePages = this._visiblePages;
|
||||
if (
|
||||
!currentVisiblePages ||
|
||||
visiblePages.length !== currentVisiblePages.length ||
|
||||
visiblePages.some((page, index) => page !== currentVisiblePages[index])
|
||||
) {
|
||||
this._visiblePages = visiblePages;
|
||||
}
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
@@ -341,7 +340,7 @@ export class DialogEnergyBatterySettings
|
||||
}
|
||||
|
||||
private _handlePowerConfigChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["power-config-changed"]>
|
||||
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
|
||||
) {
|
||||
this._powerType = ev.detail.powerType;
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -215,7 +214,7 @@ export class DialogEnergyCustomise
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleCard = (ev: HASSDomCurrentTargetEvent<HaSwitch>): void => {
|
||||
private _toggleCard = (ev: Event): void => {
|
||||
const target = ev.currentTarget as HaSwitch;
|
||||
const cardKey = target.dataset.cardKey;
|
||||
if (!cardKey) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -354,7 +353,7 @@ export class DialogEnergyGasSettings
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleCostChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
|
||||
private _handleCostChanged(ev: Event) {
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -597,9 +593,7 @@ export class DialogEnergyGridSettings
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handleImportCostTypeChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
|
||||
) {
|
||||
private _handleImportCostTypeChanged(ev: Event) {
|
||||
this._importCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
// Clear other cost fields when switching types
|
||||
this._source = {
|
||||
@@ -611,9 +605,7 @@ export class DialogEnergyGridSettings
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handleExportCostTypeChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
|
||||
) {
|
||||
private _handleExportCostTypeChanged(ev: Event) {
|
||||
this._exportCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
// Clear other cost fields when switching types
|
||||
this._source = {
|
||||
@@ -638,10 +630,9 @@ export class DialogEnergyGridSettings
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _numberCostChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
|
||||
const value = ev.currentTarget.value
|
||||
? parseFloat(ev.currentTarget.value)
|
||||
: null;
|
||||
private _numberCostChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HTMLInputElement;
|
||||
const value = input.value ? parseFloat(input.value) : null;
|
||||
this._source = { ...this._source!, number_energy_price: value };
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
@@ -662,16 +653,15 @@ export class DialogEnergyGridSettings
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _numberCompensationChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
|
||||
const value = ev.currentTarget.value
|
||||
? parseFloat(ev.currentTarget.value)
|
||||
: null;
|
||||
private _numberCompensationChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HTMLInputElement;
|
||||
const value = input.value ? parseFloat(input.value) : null;
|
||||
this._source = { ...this._source!, number_energy_price_export: value };
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handlePowerConfigChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["power-config-changed"]>
|
||||
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
|
||||
) {
|
||||
this._powerType = ev.detail.powerType;
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-checkbox";
|
||||
@@ -291,7 +290,7 @@ export class DialogEnergySolarSettings
|
||||
);
|
||||
}
|
||||
|
||||
private _handleForecastChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
|
||||
private _handleForecastChanged(ev: Event) {
|
||||
this._forecast = (ev.currentTarget as HaRadioGroup).value === "true";
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -290,7 +289,7 @@ export class DialogEnergyWaterSettings
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleCostChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
|
||||
private _handleCostChanged(ev: Event) {
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
@@ -235,7 +234,7 @@ export class HaEnergyPowerConfig extends LitElement {
|
||||
fireEvent(this, "hass-more-info", { entityId: this.helperEntityId! });
|
||||
}
|
||||
|
||||
private _handlePowerTypeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
|
||||
private _handlePowerTypeChanged(ev: Event) {
|
||||
const newPowerType = (ev.currentTarget as HaRadioGroup).value as PowerType;
|
||||
// Clear power config when switching types
|
||||
fireEvent(this, "power-config-changed", {
|
||||
|
||||
@@ -89,7 +89,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
dashboard: {
|
||||
tag: "ha-config-dashboard",
|
||||
load: () => import("./dashboard/ha-config-dashboard"),
|
||||
waitForReady: true,
|
||||
},
|
||||
entities: {
|
||||
tag: "ha-config-entities",
|
||||
|
||||
@@ -1395,7 +1395,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 40px;
|
||||
text-align: start;
|
||||
text-align: left;
|
||||
text-underline-position: from-font;
|
||||
text-decoration-skip-ink: none;
|
||||
margin: 0;
|
||||
|
||||
+2
-7
@@ -3,7 +3,6 @@ import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
@@ -452,9 +451,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
return this._formatModeLabel(scannerState.current_mode);
|
||||
}
|
||||
|
||||
private async _handleEnable(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { entry: ConfigEntry }>
|
||||
) {
|
||||
private async _handleEnable(ev: Event) {
|
||||
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
|
||||
const entryId = button.entry.entry_id;
|
||||
try {
|
||||
@@ -477,9 +474,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _openOptionFlow(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { entry: ConfigEntry }>
|
||||
) {
|
||||
private _openOptionFlow(ev: Event) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
|
||||
|
||||
+2
-7
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
@@ -212,9 +211,7 @@ class DialogMatterLockManage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleUserClick(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { user: MatterLockUser }>
|
||||
): void {
|
||||
private _handleUserClick(ev: Event): void {
|
||||
// Ignore clicks that originated from the delete button
|
||||
const path = ev.composedPath();
|
||||
if (path.some((el) => (el as HTMLElement).tagName === "HA-ICON-BUTTON")) {
|
||||
@@ -224,9 +221,7 @@ class DialogMatterLockManage extends LitElement {
|
||||
this._editUser(user);
|
||||
}
|
||||
|
||||
private _handleDeleteUserClick(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { user: MatterLockUser }>
|
||||
): void {
|
||||
private _handleDeleteUserClick(ev: Event): void {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const user = (ev.currentTarget as any).user as MatterLockUser;
|
||||
|
||||
@@ -11,7 +11,10 @@ import { addGroup, fetchGroupableDevices } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "./zha-device-endpoint-list";
|
||||
import type { ZHADeviceEndpointList } from "./zha-device-endpoint-list";
|
||||
import type {
|
||||
DeviceEndpointSelectionChangedEvent,
|
||||
ZHADeviceEndpointList,
|
||||
} from "./zha-device-endpoint-list";
|
||||
|
||||
@customElement("zha-add-group-page")
|
||||
export class ZHAAddGroupPage extends LitElement {
|
||||
@@ -128,7 +131,7 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
}
|
||||
|
||||
private _handleAddSelectionChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["selection-changed"]>
|
||||
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedDevicesToAdd = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -14,11 +14,14 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { animationStyles } from "../../../../../resources/theme/animations.globals";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button";
|
||||
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
@@ -67,6 +70,8 @@ class ZHAConfigDashboard extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _generatingBackup = false;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (!this.hass) {
|
||||
@@ -355,17 +360,18 @@ class ZHAConfigDashboard extends LitElement {
|
||||
"ui.panel.config.zha.configuration_page.download_backup_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
<ha-progress-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
.iconPath=${mdiDownload}
|
||||
.progress=${this._generatingBackup}
|
||||
@click=${this._createAndDownloadBackup}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.download_backup_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-progress-button>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
@@ -408,30 +414,32 @@ class ZHAConfigDashboard extends LitElement {
|
||||
this._configuration = await fetchZHAConfiguration(this.hass!);
|
||||
}
|
||||
|
||||
private async _createAndDownloadBackup(): Promise<void> {
|
||||
private async _createAndDownloadBackup(
|
||||
ev: HASSDomCurrentTargetEvent<HaProgressButton>
|
||||
): Promise<void> {
|
||||
// Captured up front: currentTarget is null once the first await resolves.
|
||||
const button = ev.currentTarget;
|
||||
let backup_and_metadata: ZHANetworkBackupAndMetadata;
|
||||
|
||||
// Reading the backup from the coordinator can take 5-30 seconds.
|
||||
this._generatingBackup = true;
|
||||
|
||||
try {
|
||||
backup_and_metadata = await createZHANetworkBackup(this.hass!);
|
||||
} catch (err: any) {
|
||||
button.actionError();
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to create backup",
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.backup_failed"
|
||||
),
|
||||
text: err.message,
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
this._generatingBackup = false;
|
||||
}
|
||||
|
||||
if (!backup_and_metadata.is_complete) {
|
||||
await showAlertDialog(this, {
|
||||
title: "Backup is incomplete",
|
||||
text: "A backup has been created but it is incomplete and cannot be restored. This is a coordinator firmware limitation.",
|
||||
});
|
||||
}
|
||||
|
||||
const backupJSON: string =
|
||||
"data:text/plain;charset=utf-8," +
|
||||
encodeURIComponent(JSON.stringify(backup_and_metadata.backup, null, 4));
|
||||
const backupTime: Date = new Date(
|
||||
Date.parse(backup_and_metadata.backup.backup_time)
|
||||
);
|
||||
@@ -441,7 +449,23 @@ class ZHAConfigDashboard extends LitElement {
|
||||
basename = `Incomplete ${basename}`;
|
||||
}
|
||||
|
||||
fileDownload(backupJSON, `${basename}.json`);
|
||||
const blob = new Blob(
|
||||
[JSON.stringify(backup_and_metadata.backup, null, 4)],
|
||||
{ type: "application/json" }
|
||||
);
|
||||
fileDownload(URL.createObjectURL(blob), `${basename}.json`);
|
||||
button.actionSuccess();
|
||||
|
||||
if (!backup_and_metadata.is_complete) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.backup_incomplete_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.backup_incomplete_text"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _openOptionFlow() {
|
||||
@@ -517,10 +541,6 @@ class ZHAConfigDashboard extends LitElement {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
|
||||
ha-button[size="s"] ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
.network-status div.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -4,15 +4,10 @@ import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../../../components/input/ha-input-search";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
|
||||
@@ -274,16 +269,11 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
private _handleFilterChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaInputSearch>
|
||||
): void {
|
||||
this._filter = ev.currentTarget.value ?? "";
|
||||
private _handleFilterChanged(ev: Event): void {
|
||||
this._filter = (ev.currentTarget as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _handleItemSelected(
|
||||
ev: HASSDomEvent<HASSDomEvents["ha-list-item-selected"]> &
|
||||
HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
): void {
|
||||
private _handleItemSelected(ev: CustomEvent<number>): void {
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
let selectedDeviceIds = this._selectedDeviceIds;
|
||||
|
||||
@@ -300,10 +290,7 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleItemDeselected(
|
||||
ev: HASSDomEvent<HASSDomEvents["ha-list-item-deselected"]> &
|
||||
HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
): void {
|
||||
private _handleItemDeselected(ev: CustomEvent<number>): void {
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
let selectedDeviceIds = this._selectedDeviceIds;
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ import "../../../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { formatAsPaddedHex } from "./functions";
|
||||
import "./zha-device-endpoint-list";
|
||||
import type { ZHADeviceEndpointList } from "./zha-device-endpoint-list";
|
||||
import type {
|
||||
DeviceEndpointSelectionChangedEvent,
|
||||
ZHADeviceEndpointList,
|
||||
} from "./zha-device-endpoint-list";
|
||||
import { showZHAAddGroupMembersDialog } from "./show-dialog-zha-add-group-members";
|
||||
|
||||
@customElement("zha-group-page")
|
||||
@@ -199,7 +202,7 @@ export class ZHAGroupPage extends LitElement {
|
||||
}
|
||||
|
||||
private _handleRemoveSelectionChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["selection-changed"]>
|
||||
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedDevicesToRemove = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import "../../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../../components/ha-switch";
|
||||
import type { ZHAConfiguration } from "../../../../../data/zha";
|
||||
import {
|
||||
fetchZHAConfiguration,
|
||||
@@ -21,8 +18,6 @@ import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
const PREDEFINED_TIMEOUTS = [1800, 3600, 7200, 21600, 43200, 86400];
|
||||
type ZHASwitchChangeEvent = HASSDomCurrentTargetEvent<HaSwitch>;
|
||||
type ZHAInputChangeEvent = HASSDomCurrentTargetEvent<HaInput>;
|
||||
|
||||
@customElement("zha-options-page")
|
||||
class ZHAOptionsPage extends LitElement {
|
||||
@@ -350,54 +345,53 @@ class ZHAOptionsPage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _enableIdentifyOnJoinChanged(ev: ZHASwitchChangeEvent): void {
|
||||
this._configuration!.data.zha_options.enable_identify_on_join =
|
||||
ev.currentTarget.checked;
|
||||
private _enableIdentifyOnJoinChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.enable_identify_on_join = checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _enhancedLightTransitionChanged(ev: ZHASwitchChangeEvent): void {
|
||||
this._configuration!.data.zha_options.enhanced_light_transition =
|
||||
ev.currentTarget.checked;
|
||||
private _enhancedLightTransitionChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.enhanced_light_transition = checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _lightTransitioningFlagChanged(ev: ZHASwitchChangeEvent): void {
|
||||
this._configuration!.data.zha_options.light_transitioning_flag =
|
||||
ev.currentTarget.checked;
|
||||
private _lightTransitioningFlagChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.light_transitioning_flag = checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _groupMembersAssumeStateChanged(ev: ZHASwitchChangeEvent): void {
|
||||
this._configuration!.data.zha_options.group_members_assume_state =
|
||||
ev.currentTarget.checked;
|
||||
private _groupMembersAssumeStateChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.group_members_assume_state = checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _enableMainsStartupPollingChanged(ev: ZHASwitchChangeEvent): void {
|
||||
private _enableMainsStartupPollingChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.enable_mains_startup_polling =
|
||||
ev.currentTarget.checked;
|
||||
checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _defaultLightTransitionChanged(ev: ZHAInputChangeEvent): void {
|
||||
this._configuration!.data.zha_options.default_light_transition = Number(
|
||||
ev.currentTarget.value
|
||||
);
|
||||
private _defaultLightTransitionChanged(ev: Event): void {
|
||||
const value = Number((ev.target as HTMLInputElement).value);
|
||||
this._configuration!.data.zha_options.default_light_transition = value;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _customMainsSecondsChanged(ev: ZHAInputChangeEvent): void {
|
||||
this._configuration!.data.zha_options.consider_unavailable_mains = Number(
|
||||
ev.currentTarget.value
|
||||
);
|
||||
private _customMainsSecondsChanged(ev: Event): void {
|
||||
const seconds = Number((ev.target as HTMLInputElement).value);
|
||||
this._configuration!.data.zha_options.consider_unavailable_mains = seconds;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _customBatterySecondsChanged(ev: ZHAInputChangeEvent): void {
|
||||
this._configuration!.data.zha_options.consider_unavailable_battery = Number(
|
||||
ev.currentTarget.value
|
||||
);
|
||||
private _customBatterySecondsChanged(ev: Event): void {
|
||||
const seconds = Number((ev.target as HTMLInputElement).value);
|
||||
this._configuration!.data.zha_options.consider_unavailable_battery =
|
||||
seconds;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -425,15 +419,7 @@ class ZHAOptionsPage extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private async _updateConfiguration(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & {
|
||||
progress: boolean;
|
||||
actionSuccess: () => void;
|
||||
actionError: () => void;
|
||||
}
|
||||
>
|
||||
): Promise<void> {
|
||||
private async _updateConfiguration(ev: Event): Promise<void> {
|
||||
const button = ev.currentTarget as HTMLElement & {
|
||||
progress: boolean;
|
||||
actionSuccess: () => void;
|
||||
|
||||
+1
-4
@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
@@ -479,9 +478,7 @@ class DialogZwaveCredentialUserEdit extends DirtyStateProviderMixin<CredentialFo
|
||||
}
|
||||
}
|
||||
|
||||
private _handleCredentialTypeChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
|
||||
): void {
|
||||
private _handleCredentialTypeChanged(ev: Event): void {
|
||||
this._credentialType = (ev.currentTarget as HaRadioGroup)
|
||||
.value as ZwaveCredentialType;
|
||||
// Switching types invalidates any data tied to the previous type's
|
||||
|
||||
@@ -2,13 +2,11 @@ import { mdiCloseCircle } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import {
|
||||
getZwaveNodeRawConfigParameter,
|
||||
setZwaveNodeRawConfigParameter,
|
||||
@@ -131,9 +129,9 @@ class ZWaveJSCustomParam extends LitElement {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private _customParamNumberChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
|
||||
private _customParamNumberChanged(ev: Event) {
|
||||
this._customParamNumber = this._tryParseNumber(
|
||||
ev.currentTarget.value ?? ""
|
||||
(ev.target as HTMLInputElement).value
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,8 +139,8 @@ class ZWaveJSCustomParam extends LitElement {
|
||||
this._valueSize = this._tryParseNumber(ev.detail.value) ?? 1;
|
||||
}
|
||||
|
||||
private _customValueChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
|
||||
this._value = this._tryParseNumber(ev.currentTarget.value ?? "");
|
||||
private _customValueChanged(ev: Event) {
|
||||
this._value = this._tryParseNumber((ev.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
private _customValueFormatChanged(ev: HaSelectSelectEvent) {
|
||||
|
||||
+6
-115
@@ -4,7 +4,6 @@ import {
|
||||
mdiCloseCircle,
|
||||
mdiProgressClock,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -27,10 +26,10 @@ import "../../../../../components/ha-settings-row";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type {
|
||||
ZWaveJSNodeCapabilities,
|
||||
ZWaveJSNodeConfigParam,
|
||||
ZWaveJSNodeConfigParams,
|
||||
ZWaveJSSetConfigParamResult,
|
||||
ZwaveJSNodeConfigParameterUpdate,
|
||||
ZwaveJSNodeMetadata,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import {
|
||||
@@ -39,7 +38,6 @@ import {
|
||||
fetchZwaveNodeMetadata,
|
||||
invokeZWaveCCApi,
|
||||
setZwaveNodeConfigParameter,
|
||||
subscribeZwaveNodeConfigParameterUpdates,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../../layouts/hass-error-screen";
|
||||
@@ -62,7 +60,7 @@ const icons = {
|
||||
|
||||
@customElement("zwave_js-node-config")
|
||||
class ZWaveJSNodeConfig extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@@ -86,41 +84,13 @@ class ZWaveJSNodeConfig extends LitElement {
|
||||
|
||||
@state() private _resetDialogProgress = false;
|
||||
|
||||
private _unsubConfigParamUpdates?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _resultTimeouts: Record<string, number> = {};
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._subscribeConfigParameterUpdates();
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeConfigParameterUpdates();
|
||||
this._clearAllResultTimeouts();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (!changedProps.has("route") || !this.route) {
|
||||
return;
|
||||
}
|
||||
const deviceId = this.route.path.slice(1);
|
||||
if (deviceId !== this.deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
this._config = undefined;
|
||||
this._clearAllResultTimeouts();
|
||||
this._results = {};
|
||||
this._error = undefined;
|
||||
}
|
||||
this.deviceId = this.route.path.substr(1);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>): void {
|
||||
if (changedProps.has("deviceId")) {
|
||||
this._fetchData();
|
||||
this._subscribeConfigParameterUpdates();
|
||||
} else if (!this._config) {
|
||||
if (!this._config || changedProps.has("deviceId")) {
|
||||
this._fetchData();
|
||||
}
|
||||
}
|
||||
@@ -475,58 +445,6 @@ class ZWaveJSNodeConfig extends LitElement {
|
||||
<p>${item.value}</p>`;
|
||||
}
|
||||
|
||||
private _subscribeConfigParameterUpdates(): void {
|
||||
this._unsubscribeConfigParameterUpdates();
|
||||
if (!this.isConnected || !this.hass || !this.deviceId) {
|
||||
return;
|
||||
}
|
||||
this._unsubConfigParamUpdates = subscribeZwaveNodeConfigParameterUpdates(
|
||||
this.hass,
|
||||
this.deviceId,
|
||||
this._handleConfigParameterUpdate
|
||||
);
|
||||
this._unsubConfigParamUpdates.catch(() => {
|
||||
// The backend doesn't support the subscription; the page still works,
|
||||
// it just won't receive live updates
|
||||
this._unsubConfigParamUpdates = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private _unsubscribeConfigParameterUpdates(): void {
|
||||
if (this._unsubConfigParamUpdates) {
|
||||
this._unsubConfigParamUpdates
|
||||
.then((unsub) => unsub())
|
||||
.catch(() => {
|
||||
// The subscription never succeeded, so there is nothing to clean up
|
||||
});
|
||||
this._unsubConfigParamUpdates = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleConfigParameterUpdate = (
|
||||
update: ZwaveJSNodeConfigParameterUpdate
|
||||
): void => {
|
||||
const param = this._config?.[update.id];
|
||||
if (!param) {
|
||||
return;
|
||||
}
|
||||
const status = this._results[update.id]?.status;
|
||||
if (status === "queued") {
|
||||
// The device applied the queued change; the accepted result is cleared
|
||||
// after a short delay by _setResult so the success message shows.
|
||||
// The value was already set optimistically when the change was queued,
|
||||
// so this runs even if the reported value matches the stored one.
|
||||
this._setResult(update.id, "accepted");
|
||||
} else if (status === "error" && param.value !== update.value) {
|
||||
// The parameter changed, so the previous error no longer applies
|
||||
this._setResult(update.id, undefined);
|
||||
}
|
||||
if (param.value !== update.value) {
|
||||
param.value = update.value;
|
||||
this._config = { ...this._config };
|
||||
}
|
||||
};
|
||||
|
||||
private _isEnumeratedBool(item: ZWaveJSNodeConfigParam): boolean {
|
||||
// Some Z-Wave config values use a states list with two options where index 0 = Disabled and 1 = Enabled
|
||||
// We want those to be considered boolean and show a toggle switch
|
||||
@@ -693,38 +611,16 @@ class ZWaveJSNodeConfig extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _clearResultTimeout(key: string): void {
|
||||
if (key in this._resultTimeouts) {
|
||||
clearTimeout(this._resultTimeouts[key]);
|
||||
delete this._resultTimeouts[key];
|
||||
}
|
||||
}
|
||||
|
||||
private _clearAllResultTimeouts(): void {
|
||||
Object.values(this._resultTimeouts).forEach((timeout) =>
|
||||
clearTimeout(timeout)
|
||||
);
|
||||
this._resultTimeouts = {};
|
||||
}
|
||||
|
||||
private _setResult(key: string, value: string | undefined) {
|
||||
this._clearResultTimeout(key);
|
||||
if (value === undefined) {
|
||||
delete this._results[key];
|
||||
this.requestUpdate();
|
||||
} else {
|
||||
this._results = { ...this._results, [key]: { status: value } };
|
||||
if (value === "accepted") {
|
||||
// Show the success message briefly, then clear it
|
||||
this._resultTimeouts[key] = window.setTimeout(() => {
|
||||
this._setResult(key, undefined);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _setError(key: string, message: string) {
|
||||
this._clearResultTimeout(key);
|
||||
const errorParam = { status: "error", error: message };
|
||||
this._results = { ...this._results, [key]: errorParam };
|
||||
}
|
||||
@@ -740,17 +636,12 @@ class ZWaveJSNodeConfig extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const [nodeMetadata, config, capabilities] = await Promise.all([
|
||||
let capabilities: ZWaveJSNodeCapabilities | undefined;
|
||||
[this._nodeMetadata, this._config, capabilities] = await Promise.all([
|
||||
fetchZwaveNodeMetadata(this.hass, device.id),
|
||||
fetchZwaveNodeConfigParameters(this.hass, device.id),
|
||||
fetchZwaveNodeCapabilities(this.hass, device.id),
|
||||
]);
|
||||
if (device.id !== this.deviceId) {
|
||||
// The user navigated to another node while the data was loading
|
||||
return;
|
||||
}
|
||||
this._nodeMetadata = nodeMetadata;
|
||||
this._config = config;
|
||||
this._canResetAll =
|
||||
capabilities &&
|
||||
Object.values(capabilities).some((endpoint) =>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -284,9 +283,7 @@ export class SystemLogCard extends LitElement {
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
}
|
||||
|
||||
private _openLog(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { logItem: LoggedError }>
|
||||
): void {
|
||||
private _openLog(ev: Event): void {
|
||||
const item = (ev.currentTarget as any).logItem;
|
||||
showSystemLogDetailDialog(this, { item });
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { isIPAddress } from "../../../common/string/is_ip_address";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
@@ -364,12 +363,12 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleCloud(ev: HASSDomCurrentTargetEvent<HaSwitch>) {
|
||||
private _toggleCloud(ev: Event) {
|
||||
this._cloudChecked = (ev.currentTarget as HaSwitch).checked;
|
||||
this._showCustomExternalUrl = !this._cloudChecked;
|
||||
}
|
||||
|
||||
private _toggleInternalAutomatic(ev: HASSDomCurrentTargetEvent<HaSwitch>) {
|
||||
private _toggleInternalAutomatic(ev: Event) {
|
||||
this._showCustomInternalUrl = !(ev.currentTarget as HaSwitch).checked;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@ import { mdiDeleteOutline, mdiMenuDown, mdiPlus, mdiWifi } from "@mdi/js";
|
||||
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
@@ -634,9 +630,7 @@ export class HassioNetwork extends LitElement {
|
||||
this._interface = { ...this._interfaces[this._curTabIndex] };
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaRadioGroup & { version: "ipv4" | "ipv6" }>
|
||||
): void {
|
||||
private _handleRadioValueChanged(ev: Event): void {
|
||||
const source = ev.currentTarget as HaRadioGroup;
|
||||
const value = source.value as "disabled" | "auto" | "static";
|
||||
const version = (source as any).version as "ipv4" | "ipv6";
|
||||
@@ -651,9 +645,7 @@ export class HassioNetwork extends LitElement {
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _handleRadioValueChangedAp(
|
||||
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
|
||||
): void {
|
||||
private _handleRadioValueChangedAp(ev: Event): void {
|
||||
const source = ev.currentTarget as HaRadioGroup;
|
||||
const value = source.value as "open" | "wep" | "wpa-psk";
|
||||
this._wifiConfiguration!.auth = value;
|
||||
@@ -661,11 +653,7 @@ export class HassioNetwork extends LitElement {
|
||||
this.requestUpdate("_wifiConfiguration");
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(
|
||||
ev: HASSDomTargetEvent<
|
||||
HaInput & { version: "ipv4" | "ipv6"; index: number }
|
||||
>
|
||||
): void {
|
||||
private _handleInputValueChanged(ev: Event): void {
|
||||
const source = ev.target as HaInput;
|
||||
const value = source.value;
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
@@ -703,7 +691,7 @@ export class HassioNetwork extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInputValueChangedWifi(ev: HASSDomTargetEvent<HaInput>): void {
|
||||
private _handleInputValueChangedWifi(ev: Event): void {
|
||||
const source = ev.target as HaInput;
|
||||
const value = source.value;
|
||||
const id = source.id;
|
||||
@@ -720,9 +708,7 @@ export class HassioNetwork extends LitElement {
|
||||
this._wifiConfiguration![id] = value;
|
||||
}
|
||||
|
||||
private _addAddress(
|
||||
ev: HASSDomTargetEvent<HTMLElement & { version: "ipv4" | "ipv6" }>
|
||||
): void {
|
||||
private _addAddress(ev: Event): void {
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
this._interface![version]!.address!.push(
|
||||
version === "ipv4" ? "0.0.0.0/24" : "::/64"
|
||||
@@ -731,11 +717,7 @@ export class HassioNetwork extends LitElement {
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _removeAddress(
|
||||
ev: HASSDomTargetEvent<
|
||||
HTMLElement & { index: number; version: "ipv4" | "ipv6" }
|
||||
>
|
||||
): void {
|
||||
private _removeAddress(ev: Event): void {
|
||||
const source = ev.target as any;
|
||||
const index = source.index as number;
|
||||
const version = source.version as "ipv4" | "ipv6";
|
||||
@@ -770,11 +752,7 @@ export class HassioNetwork extends LitElement {
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _removeNameserver(
|
||||
ev: HASSDomTargetEvent<
|
||||
HTMLElement & { index: number; version: "ipv4" | "ipv6" }
|
||||
>
|
||||
): void {
|
||||
private _removeNameserver(ev: Event): void {
|
||||
const source = ev.target as any;
|
||||
const index = source.index as number;
|
||||
const version = source.version as "ipv4" | "ipv6";
|
||||
|
||||
@@ -11,7 +11,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeEntityEntryName } from "../../../../common/entity/compute_entity_name";
|
||||
@@ -261,9 +260,7 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
return output;
|
||||
}
|
||||
|
||||
private _copyEntity = async (
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: HassEntity }>
|
||||
) => {
|
||||
private _copyEntity = async (ev: Event) => {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
|
||||
.entity;
|
||||
@@ -273,18 +270,14 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
});
|
||||
};
|
||||
|
||||
private _entityMoreInfo(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: HassEntity }>
|
||||
) {
|
||||
private _entityMoreInfo(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
|
||||
.entity;
|
||||
fireEvent(this, "hass-more-info", { entityId: entity.entity_id });
|
||||
}
|
||||
|
||||
private _entitySelected(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: HassEntity }>
|
||||
) {
|
||||
private _entitySelected(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
|
||||
.entity;
|
||||
|
||||
@@ -16,11 +16,7 @@ import { consume, type ContextType } from "@lit/context";
|
||||
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import {
|
||||
@@ -600,9 +596,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSearchChange(
|
||||
ev: InputEvent & HASSDomTargetEvent<HaInputSearch>
|
||||
) {
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
if (this.filter === (ev.target as HaInputSearch).value) {
|
||||
return;
|
||||
}
|
||||
@@ -712,9 +706,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
private _showStatisticsAdjustSumDialog(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { statistic: StatisticData }>
|
||||
) {
|
||||
private _showStatisticsAdjustSumDialog(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
showStatisticsAdjustSumDialog(this, {
|
||||
statistic: (
|
||||
@@ -790,11 +782,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _fixIssue = async (
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & { data: StatisticsValidationResult[] }
|
||||
>
|
||||
) => {
|
||||
private _fixIssue = async (ev: Event) => {
|
||||
const issues = (
|
||||
ev.currentTarget as HTMLElement & { data: StatisticsValidationResult[] }
|
||||
).data.sort(
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
@@ -420,7 +419,7 @@ ${
|
||||
`;
|
||||
}
|
||||
|
||||
private _splitRepositioned(ev: HASSDomTargetEvent<HaSplitPanel>) {
|
||||
private _splitRepositioned(ev: Event) {
|
||||
this._splitPosition = (ev.target as HaSplitPanel).position;
|
||||
this._storeSplitPosition();
|
||||
}
|
||||
|
||||
@@ -130,19 +130,6 @@ class PanelHome extends LitElement {
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
void import("../lovelace/strategies/home/home-dashboard-strategy").catch(
|
||||
() => undefined
|
||||
);
|
||||
void import("../lovelace/strategies/home/home-overview-view-strategy")
|
||||
.then(({ preloadHomeEnergyPreferences }) => {
|
||||
const path = this.route?.path?.split("/")[1];
|
||||
return !path || path === "overview"
|
||||
? preloadHomeEnergyPreferences(this.hass)
|
||||
: undefined;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
void import("../lovelace/views/hui-sections-view").catch(() => undefined);
|
||||
|
||||
this._updateExtraActionItems();
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
await this._loadConfigPromise;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { ChildPanelReady } from "../../layouts/panel-ready";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
@@ -32,8 +31,6 @@ class PanelLight extends LitElement {
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
private _childPanelReady?: ChildPanelReady;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
@@ -130,7 +127,6 @@ class PanelLight extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._childPanelReady ??= new ChildPanelReady(this);
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { round } from "../../../../common/number/round";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
@@ -92,8 +91,8 @@ class HuiEnergyGridBalanceCard
|
||||
|
||||
const { summedData } = getSummedData(this._data);
|
||||
|
||||
const imported = round(summedData.total.from_grid ?? 0, 3);
|
||||
const exported = round(summedData.total.to_grid ?? 0, 3);
|
||||
const imported = summedData.total.from_grid ?? 0;
|
||||
const exported = summedData.total.to_grid ?? 0;
|
||||
const net = imported - exported;
|
||||
|
||||
const fmt = (value: number) =>
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-spinner";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
@@ -39,9 +38,7 @@ export class HuiStartingCard extends LitElement implements LovelaceCard {
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner></ha-spinner>
|
||||
</ha-fade-in>
|
||||
<ha-spinner></ha-spinner>
|
||||
${this.hass.localize("ui.panel.lovelace.cards.starting.description")}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -296,6 +296,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
await this._getStatisticsMetaData(this._entityIds);
|
||||
}
|
||||
await this._getStatistics();
|
||||
if (!this.isConnected) {
|
||||
this._interval = undefined;
|
||||
return;
|
||||
}
|
||||
// statistics are created every hour
|
||||
if (!this._config?.energy_date_selection) {
|
||||
this._interval = window.setInterval(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { migrateStateColorConfig } from "../common/entity-color-config";
|
||||
import { migrateTimeFormatConfig } from "../common/entity-time-format-config";
|
||||
import { migrateSecondaryInfoConfig } from "../common/entity-secondary-info-config";
|
||||
import type {
|
||||
ConditionalRowConfig,
|
||||
LovelaceRowConfig,
|
||||
@@ -21,13 +20,11 @@ const migrateEntitiesRowConfig = (
|
||||
let newConf: LovelaceRowConfig = rowConf;
|
||||
newConf = migrateTimeFormatConfig(newConf as EntitiesCardEntityConfig);
|
||||
newConf = migrateStateColorConfig(newConf as EntitiesCardEntityConfig);
|
||||
newConf = migrateSecondaryInfoConfig(newConf as EntitiesCardEntityConfig);
|
||||
if (newConf.type === "conditional") {
|
||||
const row = (newConf as ConditionalRowConfig).row;
|
||||
if (row && typeof row === "object") {
|
||||
let newRow = migrateTimeFormatConfig(row as EntitiesCardEntityConfig);
|
||||
newRow = migrateStateColorConfig(newRow);
|
||||
newRow = migrateSecondaryInfoConfig(newRow);
|
||||
if (newRow !== row) {
|
||||
newConf = { ...newConf, row: newRow } as ConditionalRowConfig;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,16 @@ export interface EntityCardConfig extends LovelaceCardConfig {
|
||||
|
||||
export interface EntitiesCardEntityConfig extends EntityConfig {
|
||||
type?: string;
|
||||
secondary_info?: string | string[];
|
||||
secondary_info?:
|
||||
| "entity-id"
|
||||
| "last-changed"
|
||||
| "last-triggered"
|
||||
| "last-updated"
|
||||
| "area"
|
||||
| "position"
|
||||
| "state"
|
||||
| "tilt-position"
|
||||
| "brightness";
|
||||
action_name?: string;
|
||||
action?: string;
|
||||
/** @deprecated use "action" instead */
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { isCustomType } from "../../../data/lovelace_custom_cards";
|
||||
|
||||
interface LegacySecondaryInfoConfig {
|
||||
type?: string;
|
||||
secondary_info?: string | string[];
|
||||
}
|
||||
|
||||
const SUBSTITUTIONS = {
|
||||
"last-changed": "last_changed",
|
||||
"last-updated": "last_updated",
|
||||
"last-triggered": "last_triggered",
|
||||
position: "current_position",
|
||||
"tilt-position": "current_tilt_position",
|
||||
area: "area_name",
|
||||
none: undefined,
|
||||
};
|
||||
|
||||
export const migrateSecondaryInfoConfig = <T extends LegacySecondaryInfoConfig>(
|
||||
config: T
|
||||
): T => {
|
||||
// Custom elements own their config schema and may use the same option with
|
||||
// a different meaning, leave them untouched
|
||||
if (config.type !== undefined && isCustomType(config.type)) {
|
||||
return config;
|
||||
}
|
||||
if (
|
||||
config.secondary_info === undefined ||
|
||||
typeof config.secondary_info !== "string" ||
|
||||
!(config.secondary_info in SUBSTITUTIONS)
|
||||
) {
|
||||
return config;
|
||||
}
|
||||
const { secondary_info, ...rest } = config;
|
||||
return {
|
||||
...rest,
|
||||
...(SUBSTITUTIONS[secondary_info] && {
|
||||
secondary_info: SUBSTITUTIONS[secondary_info],
|
||||
}),
|
||||
} as T;
|
||||
};
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
CompareMode,
|
||||
downloadEnergyData,
|
||||
getEnergyDataCollection,
|
||||
getEnergyDefaultPeriodStorageKey,
|
||||
} from "../../../data/energy";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -544,7 +545,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _presetSelected(ev) {
|
||||
localStorage.setItem(
|
||||
`energy-default-period-_${this.collectionKey || "energy"}`,
|
||||
getEnergyDefaultPeriodStorageKey(this.hass, this.collectionKey),
|
||||
RANGE_KEYS[ev.detail.index]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@ import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { DOMAINS_INPUT_ROW } from "../../../common/const";
|
||||
import { uid } from "../../../common/util/uid";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { getEntityContext } from "../../../common/entity/context/get_entity_context";
|
||||
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-tooltip";
|
||||
@@ -17,7 +21,6 @@ import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import { createEntityNotFoundWarning } from "./hui-warning";
|
||||
import "../../../state-display/state-display";
|
||||
|
||||
@customElement("hui-generic-entity-row")
|
||||
export class HuiGenericEntityRow extends LitElement {
|
||||
@@ -37,6 +40,8 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
@property({ attribute: "catch-interaction", type: Boolean })
|
||||
public catchInteraction?;
|
||||
|
||||
private _secondaryInfoElementId = "-" + uid();
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.config) {
|
||||
return nothing;
|
||||
@@ -96,15 +101,118 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
<div class="secondary">
|
||||
${
|
||||
this.secondaryText ||
|
||||
html`<state-display
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
.content=${this.config.secondary_info}
|
||||
.timeFormat=${this.config.time_format}
|
||||
.name=${name}
|
||||
timestamp-tooltip
|
||||
>
|
||||
</state-display>`
|
||||
(this.config.secondary_info === "entity-id"
|
||||
? stateObj.entity_id
|
||||
: this.config.secondary_info === "last-changed"
|
||||
? html`
|
||||
<ha-tooltip
|
||||
for="last-changed${
|
||||
this._secondaryInfoElementId
|
||||
}"
|
||||
placement="right"
|
||||
>
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(stateObj.last_changed),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</ha-tooltip>
|
||||
<ha-relative-time
|
||||
id="last-changed${this._secondaryInfoElementId}"
|
||||
.datetime=${stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info === "last-updated"
|
||||
? html`
|
||||
<ha-tooltip
|
||||
for="last-updated${
|
||||
this._secondaryInfoElementId
|
||||
}"
|
||||
placement="right"
|
||||
>
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(stateObj.last_updated),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</ha-tooltip>
|
||||
<ha-relative-time
|
||||
id="last-updated${
|
||||
this._secondaryInfoElementId
|
||||
}"
|
||||
.datetime=${stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.config.secondary_info ===
|
||||
"last-triggered"
|
||||
? stateObj.attributes.last_triggered
|
||||
? html`
|
||||
<ha-tooltip
|
||||
for="last-triggered${
|
||||
this._secondaryInfoElementId
|
||||
}"
|
||||
placement="right"
|
||||
>
|
||||
${formatDateTimeWithSeconds(
|
||||
new Date(
|
||||
stateObj.attributes
|
||||
.last_triggered
|
||||
),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</ha-tooltip>
|
||||
<ha-relative-time
|
||||
id="last-triggered${
|
||||
this._secondaryInfoElementId
|
||||
}"
|
||||
.datetime=${
|
||||
stateObj.attributes.last_triggered
|
||||
}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.entities.never_triggered"
|
||||
)
|
||||
: this.config.secondary_info ===
|
||||
"position" &&
|
||||
stateObj.attributes.current_position !==
|
||||
undefined
|
||||
? `${this.hass.localize(
|
||||
"ui.card.cover.position"
|
||||
)}: ${stateObj.attributes.current_position}`
|
||||
: this.config.secondary_info ===
|
||||
"tilt-position" &&
|
||||
stateObj.attributes
|
||||
.current_tilt_position !== undefined
|
||||
? `${this.hass.localize(
|
||||
"ui.card.cover.tilt_position"
|
||||
)}: ${
|
||||
stateObj.attributes
|
||||
.current_tilt_position
|
||||
}`
|
||||
: this.config.secondary_info ===
|
||||
"brightness" &&
|
||||
stateObj.attributes.brightness
|
||||
? html`${Math.round(
|
||||
(stateObj.attributes.brightness /
|
||||
255) *
|
||||
100
|
||||
)}
|
||||
%`
|
||||
: this.config.secondary_info ===
|
||||
"state"
|
||||
? html`${this.hass.formatEntityState(
|
||||
stateObj
|
||||
)}`
|
||||
: this.config.secondary_info ===
|
||||
"area"
|
||||
? (this._getArea(stateObj) ??
|
||||
nothing)
|
||||
: nothing)
|
||||
}
|
||||
</div>
|
||||
`
|
||||
@@ -145,6 +253,17 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
handleAction(this, this.hass!, this.config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private _getArea(stateObj) {
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas,
|
||||
this.hass!.floors
|
||||
);
|
||||
return context.area ? computeAreaName(context.area) : undefined;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDateTimeNumeric,
|
||||
formatDateTimeWithSeconds,
|
||||
} from "../../../common/datetime/format_date_time";
|
||||
import {
|
||||
formatTime,
|
||||
@@ -30,8 +29,6 @@ import type {
|
||||
} from "../../../types";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type { TimestampRenderingFormat } from "./types";
|
||||
import { uid } from "../../../common/util/uid";
|
||||
import "../../../components/ha-tooltip";
|
||||
|
||||
const FORMATS: Record<
|
||||
string,
|
||||
@@ -63,8 +60,6 @@ class HuiTimestampDisplay extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public capitalize = false;
|
||||
|
||||
@property({ type: Boolean }) public tooltip = false;
|
||||
|
||||
@state() private _relative?: string;
|
||||
|
||||
@state()
|
||||
@@ -92,8 +87,6 @@ class HuiTimestampDisplay extends LitElement {
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
private _uid = "timestamp-display-" + uid();
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._connected = true;
|
||||
@@ -121,14 +114,6 @@ class HuiTimestampDisplay extends LitElement {
|
||||
const formatType = this._formatType;
|
||||
|
||||
if (INTERVAL_FORMAT.includes(formatType)) {
|
||||
if (this.tooltip) {
|
||||
return html`
|
||||
<ha-tooltip for=${this._uid} placement="right">
|
||||
${formatDateTimeWithSeconds(this.ts, this._locale, this._config)}
|
||||
</ha-tooltip>
|
||||
<span id=${this._uid}> ${this._relative} </span>
|
||||
`;
|
||||
}
|
||||
return html` ${this._relative} `;
|
||||
}
|
||||
if (formatType in FORMATS) {
|
||||
@@ -138,20 +123,9 @@ class HuiTimestampDisplay extends LitElement {
|
||||
: format.style && format.style in FORMATS[formatType]
|
||||
? format.style
|
||||
: "default";
|
||||
const formatted = FORMATS[formatType][style](
|
||||
this.ts,
|
||||
this._locale,
|
||||
this._config
|
||||
);
|
||||
if (this.tooltip) {
|
||||
return html`
|
||||
<ha-tooltip for=${this._uid} placement="right">
|
||||
${formatDateTimeWithSeconds(this.ts, this._locale, this._config)}
|
||||
</ha-tooltip>
|
||||
<span id=${this._uid}> ${formatted} </span>
|
||||
`;
|
||||
}
|
||||
return html` ${formatted} `;
|
||||
return html`
|
||||
${FORMATS[formatType][style](this.ts, this._locale, this._config)}
|
||||
`;
|
||||
}
|
||||
return html`${this._localize(
|
||||
"ui.panel.lovelace.components.timestamp-display.invalid_format"
|
||||
|
||||
@@ -4,8 +4,12 @@ import memoizeOne from "memoize-one";
|
||||
import { assert } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../../../data/sensor";
|
||||
import type { EntitiesCardEntityConfig } from "../../cards/types";
|
||||
@@ -13,7 +17,19 @@ import type { LovelaceRowEditor } from "../../types";
|
||||
import { entitiesConfigStruct } from "../structs/entities-struct";
|
||||
import { DOMAIN_TO_ELEMENT_TYPE } from "../../create-element/create-row-element";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../../common/const";
|
||||
import { stateContentHasTimestamp } from "../../../../state-display/state-display";
|
||||
|
||||
const SECONDARY_INFO_VALUES = {
|
||||
none: {},
|
||||
"entity-id": {},
|
||||
"last-changed": {},
|
||||
"last-updated": {},
|
||||
"last-triggered": { domains: ["automation", "script"] },
|
||||
area: {},
|
||||
position: { domains: ["cover"] },
|
||||
state: {},
|
||||
"tilt-position": { domains: ["cover"] },
|
||||
brightness: { domains: ["light"] },
|
||||
};
|
||||
|
||||
@customElement("hui-generic-entity-row-editor")
|
||||
export class HuiGenericEntityRowEditor
|
||||
@@ -31,87 +47,96 @@ export class HuiGenericEntityRowEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne((showTimeFormat?: boolean) => {
|
||||
return [
|
||||
{ name: "entity", required: true, selector: { entity: {} } },
|
||||
{
|
||||
name: "name",
|
||||
selector: { entity_name: {} },
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: {
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
include_state: true,
|
||||
include_none: true,
|
||||
private _schema = memoizeOne(
|
||||
(entity: string, localize: LocalizeFunc, showTimeFormat?: boolean) => {
|
||||
const domain = computeDomain(entity);
|
||||
|
||||
return [
|
||||
{ name: "entity", required: true, selector: { entity: {} } },
|
||||
{
|
||||
name: "name",
|
||||
selector: { entity_name: {} },
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: {
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "secondary_info",
|
||||
selector: {
|
||||
ui_state_content: {
|
||||
allow_context: true,
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
include_state: true,
|
||||
include_none: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "secondary_info",
|
||||
selector: {
|
||||
select: {
|
||||
options: (
|
||||
Object.keys(SECONDARY_INFO_VALUES).filter(
|
||||
(info) =>
|
||||
!("domains" in SECONDARY_INFO_VALUES[info]) ||
|
||||
("domains" in SECONDARY_INFO_VALUES[info] &&
|
||||
SECONDARY_INFO_VALUES[info].domains!.includes(domain))
|
||||
) as (keyof typeof SECONDARY_INFO_VALUES)[]
|
||||
).map((info) => ({
|
||||
value: info,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "time_format",
|
||||
visible: showTimeFormat,
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
});
|
||||
...(showTimeFormat
|
||||
? ([
|
||||
{
|
||||
name: "time_format",
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let schema = this.schema;
|
||||
if (!schema) {
|
||||
const entity = this._config.entity;
|
||||
const domain = entity ? computeDomain(entity) : "";
|
||||
const simpleEntity =
|
||||
(DOMAIN_TO_ELEMENT_TYPE[domain] ||
|
||||
DOMAIN_TO_ELEMENT_TYPE["_domain_not_found"]) === "simple";
|
||||
const showTimeFormat =
|
||||
(this._config.secondary_info &&
|
||||
stateContentHasTimestamp(
|
||||
entity,
|
||||
this.hass.states[entity],
|
||||
this._config.secondary_info
|
||||
)) ||
|
||||
(simpleEntity
|
||||
? TIMESTAMP_STATE_DOMAINS.has(domain)
|
||||
: domain === "event" ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
this.hass.states[entity]?.attributes.device_class
|
||||
)));
|
||||
schema = this._schema(showTimeFormat);
|
||||
}
|
||||
const entity = this._config.entity;
|
||||
const domain = entity ? computeDomain(entity) : "";
|
||||
const simpleEntity =
|
||||
(DOMAIN_TO_ELEMENT_TYPE[domain] ||
|
||||
DOMAIN_TO_ELEMENT_TYPE["_domain_not_found"]) === "simple";
|
||||
const showTimeFormat = simpleEntity
|
||||
? TIMESTAMP_STATE_DOMAINS.has(domain)
|
||||
: domain === "event" ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
this.hass.states[entity]?.attributes.device_class
|
||||
));
|
||||
|
||||
const schema =
|
||||
this.schema ||
|
||||
this._schema(this._config.entity, this.hass.localize, showTimeFormat);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -500,13 +500,20 @@ export class HuiStatisticsGraphCardEditor
|
||||
|
||||
// update card config with updated entity config
|
||||
const index = this._subElementEditorConfig!.index!;
|
||||
const oldEntityConfig = this._config!.entities[index];
|
||||
const oldEntityId =
|
||||
typeof oldEntityConfig === "string"
|
||||
? oldEntityConfig
|
||||
: oldEntityConfig?.entity;
|
||||
const newEntities = [...this._config!.entities];
|
||||
newEntities[index] = newEntityConfig;
|
||||
let config = this._config!;
|
||||
config = { ...config, entities: newEntities };
|
||||
|
||||
// remove inappropriate stat options dependently on entities
|
||||
config = await this._cleanConfig(config);
|
||||
// only a different statistic can invalidate the stat options
|
||||
if (newEntityConfig.entity !== oldEntityId) {
|
||||
config = await this._cleanConfig(config);
|
||||
}
|
||||
// normalize a generated yaml code
|
||||
config = this._orderProperties(config);
|
||||
this._config = config;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { array, union, object, string, optional, boolean } from "superstruct";
|
||||
import { union, object, string, optional, boolean } from "superstruct";
|
||||
import { timeFormatConfigStruct } from "../../components/types";
|
||||
import {
|
||||
actionConfigStruct,
|
||||
@@ -12,7 +12,7 @@ export const entitiesConfigStruct = union([
|
||||
name: optional(entityNameStruct),
|
||||
icon: optional(string()),
|
||||
image: optional(string()),
|
||||
secondary_info: optional(union([string(), array(string())])),
|
||||
secondary_info: optional(string()),
|
||||
time_format: optional(timeFormatConfigStruct),
|
||||
color: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
|
||||
@@ -12,7 +12,10 @@ class EntityRowDirective extends Directive {
|
||||
|
||||
private _name?: string;
|
||||
|
||||
private _lastHass?: HomeAssistant;
|
||||
|
||||
render(entityId: string, name: string, hass: HomeAssistant) {
|
||||
this._lastHass = hass;
|
||||
if (
|
||||
!this._element ||
|
||||
(this._entityId !== entityId &&
|
||||
@@ -22,6 +25,15 @@ class EntityRowDirective extends Directive {
|
||||
entity: entityId,
|
||||
name,
|
||||
} as LovelaceRowConfig);
|
||||
this._element.addEventListener(
|
||||
"ll-upgrade",
|
||||
() => {
|
||||
if ("hass" in this._element!) {
|
||||
this._element!.hass = this._lastHass;
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
} else if (this._entityId !== entityId || this._name !== name) {
|
||||
(this._element as LovelaceRow).setConfig?.({
|
||||
entity: entityId,
|
||||
|
||||
@@ -23,7 +23,6 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type { LovelaceRow } from "./types";
|
||||
import "../../../state-display/state-display";
|
||||
|
||||
@customElement("hui-weather-entity-row")
|
||||
class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
@@ -163,15 +162,25 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
hasSecondary
|
||||
? html`
|
||||
<div class="secondary">
|
||||
<state-display
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
.content=${this._config.secondary_info}
|
||||
.timeFormat=${this._config.time_format}
|
||||
.name=${name}
|
||||
timestamp-tooltip
|
||||
>
|
||||
</state-display>
|
||||
${
|
||||
this._config.secondary_info === "entity-id"
|
||||
? stateObj.entity_id
|
||||
: this._config.secondary_info === "last-changed"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.datetime=${stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this._config.secondary_info === "last-updated"
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.datetime=${stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
|
||||
@@ -76,7 +76,6 @@ import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-di
|
||||
import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import { ChildPanelReady } from "../../layouts/panel-ready";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
@@ -165,8 +164,6 @@ class HUIRoot extends LitElement {
|
||||
|
||||
private _restoreScroll = false;
|
||||
|
||||
private _childPanelReady?: ChildPanelReady;
|
||||
|
||||
private _undoRedoController = new UndoRedoController<UndoStackItem>(this, {
|
||||
apply: (config) => this._applyUndoRedo(config),
|
||||
currentConfig: () => ({
|
||||
@@ -780,7 +777,7 @@ class HUIRoot extends LitElement {
|
||||
huiView.narrow = this.narrow;
|
||||
}
|
||||
|
||||
let newSelectView: HUIRoot["_curView"];
|
||||
let newSelectView;
|
||||
|
||||
let viewPath: string | undefined = this.route!.path.split("/")[1];
|
||||
viewPath = viewPath ? decodeURI(viewPath) : undefined;
|
||||
@@ -1261,7 +1258,7 @@ class HUIRoot extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let view: HUIView;
|
||||
let view;
|
||||
const viewConfig = this.config.views[viewIndex];
|
||||
|
||||
if (!viewConfig) {
|
||||
@@ -1272,16 +1269,12 @@ class HUIRoot extends LitElement {
|
||||
if (this._viewCache[viewIndex]) {
|
||||
view = this._viewCache[viewIndex];
|
||||
} else {
|
||||
if (!this._childPanelReady) {
|
||||
this._childPanelReady = new ChildPanelReady(this);
|
||||
this.requestUpdate();
|
||||
}
|
||||
view = document.createElement("hui-view");
|
||||
view.index = viewIndex;
|
||||
this._viewCache[viewIndex] = view;
|
||||
}
|
||||
|
||||
view.lovelace = this.lovelace!;
|
||||
view.lovelace = this.lovelace;
|
||||
view.hass = this.hass;
|
||||
view.narrow = this.narrow;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "../../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
||||
import type { AreaRegistryEntry } from "../../../../data/area/area_registry";
|
||||
import type { EnergyPreferences } from "../../../../data/energy";
|
||||
import { getEnergyPreferences } from "../../../../data/energy";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type {
|
||||
@@ -51,26 +50,6 @@ export interface HomeOverviewViewStrategyConfig {
|
||||
shortcuts?: ShortcutItem[];
|
||||
}
|
||||
|
||||
const energyPreferencesPromises = new WeakMap<
|
||||
HomeAssistant["connection"],
|
||||
Promise<EnergyPreferences | undefined>
|
||||
>();
|
||||
|
||||
export const preloadHomeEnergyPreferences = (hass: HomeAssistant) => {
|
||||
if (!isComponentLoaded(hass.config, "energy")) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const existing = energyPreferencesPromises.get(hass.connection);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const request = getEnergyPreferences(hass).catch(() => undefined);
|
||||
energyPreferencesPromises.set(hass.connection, request);
|
||||
return request;
|
||||
};
|
||||
|
||||
const computeAreaCard = (
|
||||
areaId: string,
|
||||
hass: HomeAssistant
|
||||
@@ -326,8 +305,10 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
.filter(weatherFilter)
|
||||
.sort()[0];
|
||||
|
||||
const energyPrefs = await preloadHomeEnergyPreferences(hass);
|
||||
energyPreferencesPromises.delete(hass.connection);
|
||||
const energyPrefs = isComponentLoaded(hass.config, "energy")
|
||||
? // It raises if not configured, just swallow that.
|
||||
await getEnergyPreferences(hass).catch(() => undefined)
|
||||
: undefined;
|
||||
|
||||
const hasEnergy =
|
||||
hass.panels.energy &&
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user