Compare commits

..

32 Commits

Author SHA1 Message Date
Paul Bottein 2599f97aae Unify back navigation across subpages 2026-08-04 19:06:55 +02:00
Petar Petrov 1b6e150def Remove dead UV_THREADPOOL_SIZE assignment from gulpfile (#53490)
gulpfile.js set process.env.UV_THREADPOOL_SIZE, but gulp's CLI loads the
gulpfile through liftoff, which does async fs work first. libuv has already
sized its threadpool to the default 4 by then, so the assignment never took
effect. Measured through the gulp CLI, effective threadpool size is 3.4 both
with and without the line, and only reaches 6.9 when the variable comes from
the environment.

Hoisting it earlier cannot fix it either, since the pool is sized before any
line of gulpfile.js runs, and setting it for real measurably changes nothing.
Remove it rather than plumb the variable through the build.
2026-08-04 15:52:42 +02:00
Petar Petrov 939b7012e2 Fix wrong month in date picker calendar header (#53453) 2026-08-04 15:37:00 +02:00
Paulus Schoutsen 488d3d9b37 Revert "Show the automation state on mobile in the automations table" (#53493) 2026-08-04 15:25:41 +02:00
Petar Petrov 08dedc415f Compress faster by running zopfli in a worker pool (#53488)
Compress with zopfli in a worker pool

compress-app gzips every build artifact with zopfli, but @gfx/zopfli is
synchronous WASM: it ran on the main thread, pinned a single core and blocked
the event loop for the whole step, so it dominated the production build.

Replace gulp-zopfli-green with an equivalent gulp transform that runs the same
compressor in a pool of worker_threads sized to availableParallelism(). The
compressed output is byte-identical, and @gfx/zopfli is no longer loaded on the
main thread of every gulp invocation.

On a 12-core machine compress-app drops from 6.98 min to 1.27 min, and the full
production build from 8.87 min to 3.32 min.
2026-08-04 15:11:45 +02:00
karwosts 400bf78ce0 Color icons for "on" automations in automation-picker (#53360)
colorize active icons in automation-picker
2026-08-04 15:46:14 +03:00
Przemysław Szypowicz 9b2093125e Fix unreachable remove button on input chips with long labels (#53487)
Truncate long input chip labels

A chip with a long label, like an SSH public key in an add-on config
list option, grew past its container so the trailing remove button
ended up outside the visible area and could not be clicked. Cap the
chip at the container width so the built-in label ellipsis applies,
and show the full value as a native tooltip in the select selector.

Co-authored-by: Przemysław Szypowicz <2733699+pszypowicz@users.noreply.github.com>
2026-08-04 15:42:35 +03:00
renovate[bot] c0d963747a Update dependency @types/luxon to v3.7.3 (#53484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-04 13:14:02 +03:00
Paul Bottein 1bcd43fb75 Add form divider between actions in interaction editor (#53483) 2026-08-04 11:55:07 +02:00
Aidan Timson ce5925388c Add frontend review guidance (#53482)
* Add frontend review guidance

* Simplify frontend review skill routing

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-04 09:00:22 +00:00
jeffdunn 088f72fcc4 Fix map loading fully zoomed out when fitted before layout (#53428)
Leaflet computes the zoom level that fits a set of bounds from the current
size of the map container. When ha-map's fit runs before the browser has
laid out the container (a race that hits when the Leaflet chunks are
already cached), the container measures 0x0, the computed zoom collapses
to the minimum, and the map shows the entire world. The later
invalidateSize() from the resize observer fixes the size but keeps the
zoom, so the map stays on the world view.

Defer fitting while the container has no size and run the pending fit
once the resize observer reports a usable size.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 06:52:29 +00:00
renovate[bot] b93f09ffb2 Update dependency js-yaml to v5.2.3 (#53478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-04 09:46:52 +03:00
Matthias Alphart fb499093e5 Drop websocket collections created by an iframe panel on teardown (#53448)
* Drop websocket collections created by an iframe panel on teardown

`getCollection()` caches collections (entity registry, label registry, ...) on
the `Connection` object. A custom panel embedded in an iframe shares the
connection with the main window, so a collection the panel is the first to
request is created inside the iframe's realm.

When the user leaves the panel, `ha-panel-custom` removes the iframe and that
realm is destroyed - but the collection stays cached on the connection. Its
store, and the `setTimeout` that hands the cached state to new subscribers, are
gone, so every later subscriber waits forever for a callback that can never
fire. Since the registries moved to `LazyContextProvider` they are subscribed
on demand, which makes an iframe panel the first requester far more often: the
KNX panel's entity table renders empty after leaving and re-entering it, and
the main frontend can inherit the same dead cache.

Clean up from the iframe's own `pagehide` handler, while the realm is still
alive - `disconnectedCallback` on `ha-panel-custom` runs after the browsing
context is discarded, when `contentWindow` is already `null`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Use unknown instead of any in the CachedCollection shape

The helper never inspects the subscribed state, so the parameter type only
needs to be a placeholder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 09:31:42 +03:00
karwosts c97716d4ad Fix automation paste with variables (#53441)
* Fix automation paste with variables

* fix script

* fix stuff

* fix append

* fix script
2026-08-04 09:17:05 +03:00
dependabot[bot] b36361ca34 Bump fast-uri from 3.1.4 to 3.1.5 (#53474)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 09:03:25 +03:00
dependabot[bot] 007a7916c0 Bump undici from 6.27.0 to 6.28.0 (#53475)
Bumps [undici](https://github.com/nodejs/undici) from 6.27.0 to 6.28.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.27.0...v6.28.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.28.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 09:02:51 +03:00
renovate[bot] 637f6955a8 Update dependency @types/leaflet to v1.9.22 (#53473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-04 09:02:29 +03:00
Aidan Timson b574aaa084 Type feature event handlers (#53461)
* Type feature event handlers

* Address event handler review feedback

Co-authored-by: timmo001 <28114703+timmo001@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-04 08:28:13 +03:00
dependabot[bot] c1dca9b377 Bump postcss from 8.5.19 to 8.5.25 (#53465)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.19 to 8.5.25.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.19...8.5.25)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 08:25:14 +03:00
dependabot[bot] c9963b7f92 Bump ip-address from 10.2.0 to 10.4.0 (#53470)
Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.2.0 to 10.4.0.
- [Release notes](https://github.com/beaugunderson/ip-address/releases)
- [Commits](https://github.com/beaugunderson/ip-address/compare/v10.2.0...v10.4.0)

---
updated-dependencies:
- dependency-name: ip-address
  dependency-version: 10.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 08:24:52 +03:00
karwosts 42954152dc Fix slow loading of entity rows in device page (#53471)
Fix lazy loading of entity rows in device page
2026-08-04 08:24:19 +03:00
Petar Petrov f5a28977c2 Fix energy default period being saved under the wrong storage key (#53462) 2026-08-03 19:55:41 +02:00
renovate[bot] 9b1f407ff4 Update Node.js to v24.19.0 (#53463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 19:54:38 +02:00
Aidan Timson 277822e51c Type application event contracts (#53459)
* Type application event contracts

* Remove redundant currentTarget casts

Co-authored-by: timmo001 <28114703+timmo001@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-03 14:57:11 +00:00
Aidan Timson 4a2bca6d76 Type component event handlers (#53458)
* Type component event handlers

* Fix event handler types per review feedback

Co-authored-by: timmo001 <28114703+timmo001@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-03 17:47:02 +03:00
Aidan Timson 8e47b5369e Apply minimum release age to all dependencies (#53464) 2026-08-03 14:26:32 +00:00
Petar Petrov 759ab155c7 Show progress on the Zigbee backup button while the backup is created (#53422) 2026-08-03 15:06:53 +01:00
karwosts 8de939a696 Fix unsaved-mixin race condition (#53435) 2026-08-03 17:04:42 +03:00
Petar Petrov 5c3d74502b Stop the statistics graph card editor looping while typing a name (#53434) 2026-08-03 16:30:06 +03:00
Aidan Timson 0bf67b603c Type and Lit skills (#53457)
Document frontend type and Lit guidance
2026-08-03 16:28:49 +03:00
renovate[bot] 01915f2e5a Update dependency lint-staged to v17.3.0 (#53456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-03 14:53:15 +03:00
Aidan Timson cee1c91ed8 Home Assistant event handler types skill (#53452)
* Document Home Assistant event handler types

* Expand event handling guidance

* Prefer bottom event declarations

* Split event guidance into dedicated skill
2026-08-03 13:51:24 +03:00
192 changed files with 2514 additions and 2643 deletions
@@ -7,6 +7,8 @@ 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:
+105
View File
@@ -0,0 +1,105 @@
---
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"]>;
}
}
```
+39
View File
@@ -0,0 +1,39 @@
---
name: ha-frontend-lit
description: Home Assistant frontend Lit conventions. Use when working with reactive properties, internal state, DOM queries, lifecycle methods, or render-derived state.
---
# HA Frontend Lit
Use this skill when implementing or reviewing Lit component state, DOM access, lifecycle methods, or rendering behavior. Cross-load `ha-frontend-types` for Home Assistant data contracts, assertions, and lifecycle parameter types.
## Reactive Fields
This project currently uses Lit's TypeScript experimental decorators with `useDefineForClassFields: false`. Match existing declarations and do not introduce standard-decorator `accessor` syntax unless the project changes decorator mode.
- Use `@property()` for public reactive API and `@state()` for private reactive state.
- Prefer inferred types for initialized reactive fields when inference preserves the intended type; annotate when widening or an external contract requires it.
## DOM Queries
Prefer Lit's `@query()` or `@queryAll()` decorators for fixed selectors in the component's render root.
- Type the decorated field with the narrowest useful DOM or component interface.
- Keep the field optional when it may be absent at the point of access, including conditional rendering or pre-render lifecycle access.
- Use a definite assignment assertion only when every call site runs after the node is guaranteed to exist.
- The optional second argument to `@query()`, as in `@query("#target", true)`, caches the first query result. Use it only when later renders cannot replace the queried node.
- Use a direct query when the selector is dynamic or the target is outside the component's render root. Before querying a child, consider whether the required value belongs in parent state or data flow.
## Render-Derived State
- Prefer render-local values for inexpensive structures used only by that render.
- Assign a render-local value once when repeated evaluation is non-trivial or a local name improves clarity.
- Keep purely presentational derivations in `render()`. Use stored state or `willUpdate()` when the value must participate in lifecycle work, reflection, CSS, or non-render consumers.
- Use `memoizeOne` for pure, argument-derived transforms when stable input identity avoids meaningful repeated work. Keep inputs explicit and limited, and do not add caching without a credible benefit over computing the value directly.
## References
- [Reactive properties](https://lit.dev/docs/components/properties/)
- [Decorators](https://lit.dev/docs/components/decorators/)
- [Shadow DOM queries](https://lit.dev/docs/components/shadow-dom/#query)
- [Reactive update cycle](https://lit.dev/docs/components/lifecycle/)
+28 -1
View File
@@ -31,6 +31,33 @@ When creating a pull request, use `.github/PULL_REQUEST_TEMPLATE.md` as the body
## Recurring Review Issues
Scope and public surface:
- Keep changes independently reviewable and limited to the requested area.
- Prefer existing Home Assistant helpers, Lit primitives, and component seams over parallel implementations.
- Challenge new public properties and optional feature surface when transient options or existing seams meet the requirement with less lifecycle and consistency cost.
Stateful and asynchronous UI:
- Review transitions in both directions, not only individual rendered states.
- When controls reappear, restore valid defaults instead of retaining state that was only valid while they were hidden.
- Establish immutable dirty-state baselines before asynchronous work, guard against stale responses, and preserve unsaved state in mounted editors.
- Determine an action's current meaning before applying dirty-state checks, especially when an action can change between Save and Close.
Readiness and invalidation:
- Treat readiness as the first displayable terminal result, including stable empty and error states.
- Register child readiness before resolving the parent, do not treat fallback work as terminal, and replay readiness correctly for cached or reused panels.
- Ensure every value read by memoized output participates in its invalidation.
Repository-owned contracts:
- Consult the public [frontend developer documentation](https://developers.home-assistant.io/docs/frontend/) for documented architecture, data flow, design, and development workflows.
- For new leaf components, load `ha-frontend-contexts` and verify they consume narrow contexts instead of introducing a broad `hass` property; containers and external APIs may still require `hass`.
- Verify backend assumptions against the owning Core, Supervisor, or WebSocket implementation, and component assumptions against the exported component contract.
- Prefer canonical repository helpers and test setup over duplicate local implementations.
- Promote AI-review concerns into durable guidance only when supported by code evidence, reproduced behavior, an accepted corrective commit, or human-maintainer validation.
User experience and accessibility:
- Forms need proper labels, helper text, and validation feedback.
@@ -70,4 +97,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-styling`, `ha-frontend-testing`, or `ha-frontend-user-facing-text` when a finding falls in that area.
- Load the matching `ha-frontend-*` skill when a finding falls within its area.
+44
View File
@@ -0,0 +1,44 @@
---
name: ha-frontend-types
description: Home Assistant frontend TypeScript conventions. Use when defining or reviewing backend data contracts, optional schemas, shared types, assertions, or Lit lifecycle types.
---
# HA Frontend Types
Use this skill for Home Assistant-specific TypeScript contracts and type choices.
## Home Assistant Data Contracts
Verify data contracts against the source that owns them, such as Home Assistant Core, Supervisor, a WebSocket handler, or an exported component type. Do not shape a type around assumptions made by its current frontend consumers.
- Match required, optional, nullable, and defaulted fields to the producer and runtime contract. Preserve optional configuration fields when omission is supported and has defined behavior.
- Use distinct request and response types when their wire shapes differ.
- When changing a shared contract, check affected consumers, tests, fixtures, and mocks.
## Reuse Home Assistant Contracts
- Reuse the canonical Home Assistant type when one exists. Define shared contract types in the data or API module that owns them.
- Reuse types exported by components and helpers rather than reconstructing their payloads. For event types, follow `ha-frontend-events`.
- When one domain contract is used across modules, define and export it from the module that owns that contract.
Prefer an existing owning contract. Introduce a frontend-specific type when the frontend shape or boundary genuinely differs.
## Assertions
Prefer accurate types and runtime narrowing. Use assertions or TypeScript suppressions at boundaries where the runtime invariant is understood but cannot be expressed cleanly; keep them narrow and explain non-obvious invariants.
## Lit Lifecycle Types
For Lit lifecycle methods that receive changed properties, use `PropertyValues<this>` when the method only needs public reactive properties:
```ts
protected willUpdate(changedProperties: PropertyValues<this>) {
// ...
}
```
Use unparameterized `PropertyValues` when the method inspects private or protected reactive properties, which are not keys of `this`. Do not add assertions solely to retain `PropertyValues<this>`.
## Enforced Baseline
Use `import type` for type-only imports. This is enforced by the repository ESLint configuration.
+1 -1
View File
@@ -1 +1 @@
24.18.1
24.19.0
+3
View File
@@ -40,6 +40,9 @@ 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-types`: backend data contracts, optional schemas, shared types, assertions, and lifecycle types.
- `ha-frontend-lit`: reactive fields, DOM queries, lifecycle behavior, and render-derived state.
- `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.
+1 -1
View File
@@ -3,8 +3,8 @@
import { constants } from "node:zlib";
import gulp from "gulp";
import brotli from "gulp-brotli";
import zopfli from "gulp-zopfli-green";
import paths from "../paths.cjs";
import zopfli from "../zopfli.mjs";
const filesGlob = "*.{js,json,css,svg,xml}";
const brotliOptions = {
+18
View File
@@ -0,0 +1,18 @@
// Worker side of the zopfli pool. @gfx/zopfli is a synchronous WASM build, so
// compressing on the main thread blocks the event loop; one instance per worker
// is what makes the work parallel.
import { parentPort } from "node:worker_threads";
import zopfli from "@gfx/zopfli";
parentPort.on("message", ({ contents, options }) => {
zopfli.gzip(contents, options, (error, result) => {
if (error) {
parentPort.postMessage({ error: error.message ?? String(error) });
return;
}
// `result` is a fresh Uint8Array copied out of the WASM heap, so it owns
// its ArrayBuffer and can be transferred instead of cloned.
parentPort.postMessage({ result }, [result.buffer]);
});
});
+196
View File
@@ -0,0 +1,196 @@
// Gulp transform that gzips files with zopfli across a pool of worker threads.
//
// Drop-in replacement for gulp-zopfli-green. That plugin compresses on the main
// thread, and @gfx/zopfli is synchronous WASM, so it pins a single core and
// blocks the event loop for the whole compression step. Running one WASM
// instance per worker parallelises it; the output bytes are unchanged.
import { availableParallelism } from "node:os";
import { Transform } from "node:stream";
import { buffer as readStream } from "node:stream/consumers";
import { Worker } from "node:worker_threads";
const WORKER_URL = new URL("./zopfli-worker.mjs", import.meta.url);
const EXTENSION = ".gz";
// Left empty on purpose: @gfx/zopfli then applies its own defaults, which is
// what gulp-zopfli-green did, so compressed output stays byte-identical.
const ZOPFLI_OPTIONS = {};
const poolSize = () => {
const configured = Number(process.env.ZOPFLI_WORKERS);
return Number.isInteger(configured) && configured > 0
? configured
: availableParallelism();
};
// One job per worker at a time, so the worker itself is the job slot.
const createPool = (size) => {
const live = new Set();
const idle = [];
const waiting = [];
const inFlight = new Map();
// A dead worker must leave the pool, or it gets handed a job that never
// completes and the build hangs instead of failing.
const retire = (worker, error) => {
if (!live.delete(worker)) {
return;
}
const index = idle.indexOf(worker);
if (index !== -1) {
idle.splice(index, 1);
}
const job = inFlight.get(worker);
inFlight.delete(worker);
job?.reject(error);
waiting.shift()?.();
};
const spawn = () => {
const worker = new Worker(WORKER_URL);
live.add(worker);
// Idle workers must not hold the process open. Each job refs its worker for
// as long as it runs, so a pending compression always keeps the event loop
// alive.
worker.unref();
worker.on("message", ({ error, result }) => {
const job = inFlight.get(worker);
if (!job) {
return;
}
inFlight.delete(worker);
worker.unref();
idle.push(worker);
if (error) {
job.reject(new Error(error));
} else {
job.resolve(
Buffer.from(result.buffer, result.byteOffset, result.byteLength)
);
}
waiting.shift()?.();
});
worker.on("error", (error) => retire(worker, error));
worker.on("exit", () =>
retire(worker, new Error("zopfli worker exited unexpectedly"))
);
return worker;
};
return (contents) =>
new Promise((resolve, reject) => {
const start = () => {
const worker = idle.pop() ?? (live.size < size ? spawn() : undefined);
if (!worker) {
waiting.push(start);
return;
}
inFlight.set(worker, { resolve, reject });
worker.ref();
// `contents` is cloned rather than transferred: buffers read by vinyl
// can share a pooled ArrayBuffer with unrelated buffers, and
// transferring would detach those too.
worker.postMessage({ contents, options: ZOPFLI_OPTIONS });
};
start();
});
};
// Shared by every transform this module hands out, so the worker count is a
// property of the process rather than of how many streams happen to run.
let pool;
const sharedPool = () => {
if (!pool) {
const size = poolSize();
pool = { size, compress: createPool(size) };
}
return pool;
};
// through2 and node's Transform both wait for the previous callback before
// handling the next file, which would serialise the pool down to one worker.
// This keeps `limit` files in flight and applies backpressure beyond that.
class ParallelTransform extends Transform {
#limit;
#handle;
#inFlight = 0;
#resume;
#finish;
constructor(limit, handle) {
super({ objectMode: true, highWaterMark: limit });
this.#limit = limit;
this.#handle = handle;
}
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
_transform(file, _encoding, callback) {
this.#inFlight += 1;
this.#handle(file)
.then((result) => {
if (result) {
this.push(result);
}
})
.catch((error) => this.destroy(error))
.finally(() => {
this.#inFlight -= 1;
const resume = this.#resume;
this.#resume = undefined;
resume?.();
if (this.#inFlight === 0) {
const finish = this.#finish;
this.#finish = undefined;
finish?.();
}
});
if (this.#inFlight < this.#limit) {
callback();
} else {
this.#resume = callback;
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
_flush(callback) {
if (this.#inFlight === 0) {
callback();
} else {
this.#finish = callback;
}
}
}
/**
* @param {object} [options]
* @param {number} [options.threshold] Skip files smaller than this many bytes.
*/
export default ({ threshold = 0 } = {}) => {
const { size, compress } = sharedPool();
return new ParallelTransform(size, async (file) => {
if (file.isNull()) {
return file;
}
if (file.isStream()) {
file.contents = await readStream(file.contents);
}
if (threshold && file.contents.length < threshold) {
// Passed through unrenamed and uncompressed, as gulp-zopfli-green did.
return file;
}
file.contents = await compress(file.contents);
file.path += EXTENSION;
return file;
});
};
+29 -1
View File
@@ -111,7 +111,17 @@ export default tseslint.config(
"no-bitwise": "error",
"no-console": "error",
"no-restricted-globals": [2, "event"],
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"no-restricted-syntax": [
"error",
"LabeledStatement",
"WithStatement",
{
selector:
"CallExpression[callee.property.name=/^(push|replace)State$/]",
message:
"Use navigate(), updateHistoryState() or replaceCurrentUrl() from common/navigate. History entries carry the app's own bookkeeping, which a raw pushState/replaceState drops.",
},
],
"wc/no-self-class": "off",
// import-x rules
@@ -222,6 +232,24 @@ export default tseslint.config(
],
},
},
{
// These own history entries themselves: the navigation helpers, the dialog
// stack, the boot paths that run before the app has any state to keep, and
// the tests that fabricate entries to simulate a document load.
files: [
"src/common/navigate.ts",
"src/dialogs/make-dialog-manager.ts",
"src/state/url-sync-mixin.ts",
"src/panels/config/automation/add-automation-element-dialog.ts",
"src/entrypoints/core.ts",
"src/onboarding/**/*.ts",
"cast/**/*.ts",
"test/**/*.ts",
],
rules: {
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
},
},
{
files: ["src/util/recorder-worklet.js"],
languageOptions: {
@@ -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
-3
View File
@@ -1,4 +1 @@
import { availableParallelism } from "node:os";
import "./build-scripts/gulp/index.mjs";
process.env.UV_THREADPOOL_SIZE = availableParallelism();
+6 -6
View File
@@ -103,12 +103,11 @@
"echarts": "6.1.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.5.0",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.3.0",
"intl-messageformat": "11.2.13",
"js-yaml": "5.2.2",
"js-yaml": "5.2.3",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
@@ -145,6 +144,7 @@
"@babel/preset-env": "8.0.2",
"@bundle-stats/plugin-webpack-filter": "4.22.2",
"@eslint/js": "10.0.1",
"@gfx/zopfli": "1.0.15",
"@html-eslint/eslint-plugin": "0.64.0",
"@lokalise/node-api": "16.3.0",
"@octokit/auth-oauth-device": "8.0.3",
@@ -160,11 +160,11 @@
"@types/color-name": "2.0.0",
"@types/culori": "4.0.1",
"@types/html-minifier-terser": "7.0.2",
"@types/leaflet": "1.9.21",
"@types/leaflet": "1.9.22",
"@types/leaflet-draw": "1.0.13",
"@types/leaflet.markercluster": "1.5.6",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.2",
"@types/luxon": "3.7.3",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
@@ -196,7 +196,7 @@
"jsdom": "30.0.1",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "5.0.1",
"lint-staged": "17.2.0",
"lint-staged": "17.3.0",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.18.1",
@@ -230,6 +230,6 @@
},
"packageManager": "yarn@4.18.0",
"volta": {
"node": "24.18.1"
"node": "24.19.0"
}
}
+2 -2
View File
@@ -8,10 +8,10 @@
":prConcurrentLimit10",
":semanticCommitsDisabled",
"group:monorepos",
"group:recommended",
"security:minimumReleaseAgeNpm"
"group:recommended"
],
"enabledManagers": ["npm", "nvm", "custom.regex"],
"minimumReleaseAge": "3 days",
"postUpdateOptions": ["yarnDedupeHighest"],
"lockFileMaintenance": {
"description": ["Run after patch releases but before next beta"],
+23
View File
@@ -294,3 +294,26 @@ export const formatCallyDateRange = (
return `${startDate}/${endDate}`;
};
/**
* August 2021, for calendar days coming out of cally.
*
* cally hands out calendar days as `Date` objects anchored to UTC midnight, and
* formats its own headings in UTC too. Anything derived from a cally event has
* to be formatted the same way: the resolved time zone shifts the day back for
* negative UTC offsets, which becomes a wrong month — and in January a wrong
* year — whenever the day is the 1st.
*/
export const formatCallyMonthYear = (
dateObj: Date,
locale: FrontendLocaleData
) => formatCallyMonthYearMem(locale.language).format(dateObj);
const formatCallyMonthYearMem = memoizeOne(
(language: string) =>
new Intl.DateTimeFormat(language, {
month: "long",
year: "numeric",
timeZone: "UTC",
})
);
+2 -1
View File
@@ -1,8 +1,9 @@
import type { ReactiveElement } from "lit";
import { updateHistoryState } from "../navigate";
import { throttle } from "../util/throttle";
const throttleReplaceState = throttle((value) => {
history.replaceState({ scrollPosition: value }, "");
updateHistoryState({ scrollPosition: value });
}, 300);
export function restoreScroll(selector: string) {
+59 -39
View File
@@ -1,6 +1,7 @@
import { closeAllDialogs } from "../dialogs/make-dialog-manager";
import { fireEvent } from "./dom/fire_event";
import { mainWindow } from "./dom/get_main_window";
import { currentPath } from "./url/current-path";
declare global {
// for fire event
@@ -17,6 +18,26 @@ export interface NavigateOptions {
// max time to wait for dialogs to close before navigating
const DIALOG_WAIT_TIMEOUT = 500;
/**
* Merge into the current history entry's state, keeping what is already there.
* Entries carry the app's own bookkeeping (`from`, `root`, dialog state), so
* they must never be replaced wholesale.
*/
export const updateHistoryState = (patch: Record<string, unknown>) => {
mainWindow.history.replaceState(
{ ...mainWindow.history.state, ...patch },
""
);
};
/**
* Rewrite the URL of the current history entry without navigating and without
* touching its state. For query parameter cleanup.
*/
export const replaceCurrentUrl = (url: string) => {
mainWindow.history.replaceState(mainWindow.history.state, "", url);
};
/**
* Stash a destination URL in the current history entry's state. If the page
* is refreshed while a dialog is open, urlSyncMixin will navigate to this URL
@@ -24,10 +45,7 @@ const DIALOG_WAIT_TIMEOUT = 500;
* The current URL is not changed.
*/
export const setRefreshUrl = (path: string) => {
mainWindow.history.replaceState(
{ ...mainWindow.history.state, refreshUrl: path },
""
);
updateHistoryState({ refreshUrl: path });
};
/**
@@ -63,37 +81,32 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
}
const replace = options?.replace || false;
if (__DEMO__) {
if (!path.includes("#")) {
// The demo routes with the hash instead of the pathname. Resolve the
// path like the browser would do for pushState, and keep the query
// parameters in the URL query instead of inside the hash.
const url = new URL(
path,
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
);
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
}
if (replace) {
mainWindow.history.replaceState(
mainWindow.history.state?.root
? { root: true }
: (options?.data ?? null),
"",
path
);
} else {
mainWindow.history.pushState(options?.data ?? null, "", path);
}
} else if (replace) {
mainWindow.history.replaceState(
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
if (__DEMO__ && !path.includes("#")) {
// The demo routes with the hash instead of the pathname. Resolve the
// path like the browser would do for pushState, and keep the query
// parameters in the URL query instead of inside the hash.
const url = new URL(
path,
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
);
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
}
const { history } = mainWindow;
if (replace) {
// A replaced entry keeps its predecessor, so it keeps `from`.
const { root, from } = history.state ?? {};
const state = root ? { root: true } : (options?.data ?? null);
history.replaceState(
from === undefined ? state : { ...state, from },
"",
path
);
} else {
mainWindow.history.pushState(options?.data ?? null, "", path);
history.pushState({ ...options?.data, from: currentPath() }, "", path);
}
fireEvent(mainWindow, "location-changed", {
replace,
});
@@ -101,8 +114,17 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
};
/**
* Navigate back in history, with fallback to a default path if no history exists.
* This prevents a user from getting stuck when they navigate directly to a page with no history.
* Whether the previous history entry is a page this app navigated away from.
* `history.length` cannot answer this: a login redirect goes through
* `location.assign`, which leaves /auth/authorize right behind the requested
* page, and going back there would bounce the user out of the app.
*/
export const canGoBack = (): boolean =>
mainWindow.history.state?.from !== undefined;
/**
* Navigate back to the page we came from, falling back to a path when the
* previous entry is not ours (deep link, login redirect, fresh tab).
*/
export const goBack = async (fallbackPath?: string): Promise<void> => {
const canProceed = await ensureDialogsClosed(Date.now());
@@ -110,14 +132,12 @@ export const goBack = async (fallbackPath?: string): Promise<void> => {
return;
}
// Check if we have history to go back to
const { history } = mainWindow;
if (history.length > 1) {
history.back();
// Read after closing dialogs: their history entries are popped by then, so
// this is the state of the page entry.
if (canGoBack()) {
mainWindow.history.back();
return;
}
// No history available, navigate to fallback path
const fallback = fallbackPath || "/";
navigate(fallback, { replace: true });
navigate(fallbackPath || "/", { replace: true });
};
+10
View File
@@ -0,0 +1,10 @@
import { mainWindow } from "../dom/get_main_window";
/**
* The path of the page currently shown by the app. The demo routes with the
* hash instead of the pathname, see navigate().
*/
export const currentPath = (): string =>
__DEMO__
? mainWindow.location.hash.substring(1)
: mainWindow.location.pathname;
@@ -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,6 +121,12 @@ export class HaProgressButton extends LitElement {
width: 100%;
}
/* 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;
}
/* Fade the content out rather than hiding it, so the button keeps its
accessible name while the result icon covers it. */
ha-button.result::part(start),
+11 -4
View File
@@ -26,7 +26,10 @@ import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
import { getAllGraphColors } from "../../common/color/colors";
import { transform } from "../../common/decorators/transform";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { afterNextRender } from "../../common/util/render-status";
@@ -1216,7 +1219,9 @@ export class HaChartBase extends LitElement {
}
// Long-press to solo on touch/pen devices (500ms, consistent with action-handler-directive)
private _legendPointerDown(ev: PointerEvent) {
private _legendPointerDown(
ev: PointerEvent & HASSDomCurrentTargetEvent<HTMLElement>
) {
// Mouse uses Ctrl/Cmd+click instead
if (ev.pointerType === "mouse") {
return;
@@ -1246,7 +1251,9 @@ export class HaChartBase extends LitElement {
}
}
private _toggleDataset(ev: MouseEvent) {
private _toggleDataset(
ev: MouseEvent & HASSDomCurrentTargetEvent<HTMLElement>
) {
ev.stopPropagation();
if (!this.chart) {
return;
@@ -1268,7 +1275,7 @@ export class HaChartBase extends LitElement {
this._handleDatasetToggle(id);
}
private _labelClick(ev: MouseEvent) {
private _labelClick(ev: MouseEvent & HASSDomCurrentTargetEvent<HTMLElement>) {
ev.stopPropagation();
if (!this.chart) {
return;
+8 -7
View File
@@ -2,13 +2,10 @@ import { customElement, property, state } from "lit/decorators";
import { LitElement, html, css } from "lit";
import type { EChartsType } from "echarts/core";
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
import type {
CallbackDataParams,
ECElementEvent,
} from "echarts/types/src/util/types";
import type { CallbackDataParams } from "echarts/types/src/util/types";
import memoizeOne from "memoize-one";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { fireEvent } from "../../common/dom/fire_event";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import SankeyChart from "../../resources/echarts/components/sankey/install";
import type { HomeAssistant } from "../../types";
import type { HaECOption } from "../../resources/echarts/echarts";
@@ -121,11 +118,15 @@ export class HaSankeyChart extends LitElement {
return null;
};
private _handleChartSankeyRoam = (ev: CustomEvent) => {
private _handleChartSankeyRoam = (
ev: HASSDomEvent<HASSDomEvents["chart-sankeyroam"]>
) => {
this._currentZoom = ev.detail.zoom;
};
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
private _handleChartClick = (
ev: HASSDomEvent<HASSDomEvents["chart-click"]>
) => {
const detail = ev.detail;
// Only handle node clicks (not links)
if (detail.dataType !== "node") {
@@ -216,11 +216,13 @@ export class StateHistoryChartLine extends LitElement {
})}`;
};
private _datasetHidden(ev: CustomEvent) {
private _datasetHidden(ev: HASSDomEvent<HASSDomEvents["dataset-hidden"]>) {
this._hiddenStats.add(ev.detail.id);
}
private _datasetUnhidden(ev: CustomEvent) {
private _datasetUnhidden(
ev: HASSDomEvent<HASSDomEvents["dataset-unhidden"]>
) {
this._hiddenStats.delete(ev.detail.id);
}
@@ -229,7 +231,7 @@ export class StateHistoryChartLine extends LitElement {
chartBase.zoom(start, end, true);
}
private _handleDataZoom(ev: CustomEvent) {
private _handleDataZoom(ev: HASSDomEvent<HASSDomEvents["chart-zoom"]>) {
fireEvent(this, "chart-zoom-with-index", {
start: ev.detail.start ?? 0,
end: ev.detail.end ?? 100,
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import type {
CustomSeriesOption,
CustomSeriesRenderItem,
ECElementEvent,
TooltipPositionCallbackParams,
} from "echarts/types/dist/shared";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
@@ -21,7 +20,7 @@ import echarts from "../../resources/echarts/echarts";
import { luminosity } from "../../common/color/rgb";
import { hex2rgb } from "../../common/color/convert-color";
import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
@customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement {
@@ -271,7 +270,7 @@ export class StateHistoryChartTimeline extends LitElement {
chartBase.zoom(start, end, true);
}
private _handleDataZoom(ev: CustomEvent) {
private _handleDataZoom(ev: HASSDomEvent<HASSDomEvents["chart-zoom"]>) {
fireEvent(this, "chart-zoom-with-index", {
start: ev.detail.start ?? 0,
end: ev.detail.end ?? 100,
@@ -385,7 +384,9 @@ export class StateHistoryChartTimeline extends LitElement {
this._chartData = datasets;
}
private _handleChartClick(e: CustomEvent<ECElementEvent>): void {
private _handleChartClick(
e: HASSDomEvent<HASSDomEvents["chart-click"]>
): void {
if (e.detail.targetType === "axisLabel") {
const dataset = this._chartData[e.detail.dataIndex];
if (dataset) {
+7 -3
View File
@@ -11,6 +11,10 @@ import {
} from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import type {
HASSDomEvent,
HASSDomTargetEvent,
} from "../../common/dom/fire_event";
import type {
HistoryResult,
LineChartUnit,
@@ -316,13 +320,13 @@ export class StateHistoryCharts extends LitElement {
}
}
private _yWidthChanged(e: CustomEvent<HASSDomEvents["y-width-changed"]>) {
private _yWidthChanged(e: HASSDomEvent<HASSDomEvents["y-width-changed"]>) {
this._childYWidths[e.detail.chartIndex] = e.detail.value;
this._maxYWidth = Math.max(...Object.values(this._childYWidths), 0);
}
private _handleTimelineSync(
e: CustomEvent<HASSDomEvents["chart-zoom-with-index"]>
e: HASSDomEvent<HASSDomEvents["chart-zoom-with-index"]>
) {
if (!this.syncCharts || this._isSyncing) {
return;
@@ -384,7 +388,7 @@ export class StateHistoryCharts extends LitElement {
}
@eventOptions({ passive: true })
private _saveScrollPos(e: Event) {
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
}
+4 -2
View File
@@ -204,12 +204,14 @@ export class StatisticsChart extends LitElement {
`;
}
private _datasetHidden(ev: CustomEvent) {
private _datasetHidden(ev: HASSDomEvent<HASSDomEvents["dataset-hidden"]>) {
this._hiddenStats.add(ev.detail.id);
this.requestUpdate("_hiddenStats");
}
private _datasetUnhidden(ev: CustomEvent) {
private _datasetUnhidden(
ev: HASSDomEvent<HASSDomEvents["dataset-unhidden"]>
) {
this._hiddenStats.delete(ev.detail.id);
this.requestUpdate("_hiddenStats");
}
+1
View File
@@ -15,6 +15,7 @@ export class HaInputChip extends InputChip {
styles,
css`
:host {
max-width: 100%;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-sys-color-on-surface-variant: var(--primary-text-color);
+37 -41
View File
@@ -9,12 +9,16 @@ import { customElement, property, queryAll, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import {
formatCallyDateRange,
formatDateMonth,
formatDateYear,
formatCallyMonthYear,
formatDateMonthYear,
formatISODateOnly,
} from "../../common/datetime/format_date";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import type {
HASSDomEvent,
HASSDomTargetEvent,
} from "../../common/dom/fire_event";
import { configContext, internationalizationContext } from "../../data/context";
import { TimeZone } from "../../data/translation";
import { MobileAwareMixin } from "../../mixins/mobile-aware-mixin";
@@ -56,13 +60,16 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
})
private _hassConfig!: HassConfig;
/** used to show month in calendar-range header */
@state() private _pickerMonth?: string;
/** used to show month and year in calendar-range header */
@state() private _pickerMonthYear?: string;
/** used to show year in calendar-date header */
@state() private _pickerYear?: string;
/** used for today to navigate focus in calendar-range */
/**
* used for today to navigate focus in calendar-range
*
* Always mirrors the day calendar-range has focused, never cleared: once this
* has held a date, rendering `undefined` over it makes cally fall back to the
* selected range and page the calendar away from where the user is.
*/
@state() private _focusDate?: string;
@state() private _dateValue?: string;
@@ -88,12 +95,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
this._hassConfig
)
: undefined;
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
this._pickerMonthYear = formatDateMonthYear(
date,
this._i18n.locale,
this._hassConfig
@@ -163,9 +165,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
slot="previous"
></ha-icon-button-prev>
<div class="heading" slot="heading">
<span class="month-year"
>${this._pickerMonth} ${this._pickerYear}</span
>
<span class="month-year">${this._pickerMonthYear}</span>
<ha-icon-button
@click=${this._focusToday}
.path=${mdiCalendarToday}
@@ -229,12 +229,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
this._pickerMonthYear = formatDateMonthYear(
date,
this._i18n.locale,
this._hassConfig
@@ -307,28 +302,27 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
});
}
private _focusChanged(ev: CustomEvent<Date>) {
const date = ev.detail;
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._focusDate = undefined;
private _focusChanged(
ev: HASSDomEvent<Date> &
HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
) {
this._pickerMonthYear = formatCallyMonthYear(ev.detail, this._i18n.locale);
this._focusDate = ev.target.focusedDate;
}
private _handleChange(ev: CustomEvent) {
private _handleChange(
ev: HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
) {
const dateElement = ev.target as HTMLElementTagNameMap["calendar-range"];
this._dateValue = dateElement.value;
this._focusDate = undefined;
this._focusDate = dateElement.focusedDate;
}
private _clickDateRangeChip(ev: Event) {
private _clickDateRangeChip(
ev: HASSDomTargetEvent<
HaFilterChip & { index: number; range: [Date, Date] }
>
) {
const chip = ev.target as HaFilterChip & {
index: number;
range: [Date, Date];
@@ -336,7 +330,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
this._saveDateRangePreset(chip.range, chip.index);
}
private _setDateRange(ev: CustomEvent<ActionDetail>) {
private _setDateRange(ev: HASSDomEvent<ActionDetail>) {
const dateRange: [Date, Date] = Object.values(this.ranges!)[
ev.detail.index
];
@@ -355,7 +349,9 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
});
}
private _handleChangeTime(ev: ValueChangedEvent<string>) {
private _handleChangeTime(
ev: ValueChangedEvent<string> & HASSDomTargetEvent<HaBaseTimeInput>
) {
ev.stopPropagation();
const time = ev.detail.value;
const target = ev.target as HaBaseTimeInput;
@@ -6,13 +6,18 @@ import type { HassConfig } from "home-assistant-js-websocket/dist/types";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import {
formatDateMonth,
formatCallyMonthYear,
formatDateMonthYear,
formatDateShort,
formatDateYear,
formatISODateOnly,
} from "../../common/datetime/format_date";
import { valueToDate } from "../../common/datetime/value_to_date";
import { transform } from "../../common/decorators/transform";
import type {
HASSDomEvent,
HASSDomTargetEvent,
} from "../../common/dom/fire_event";
import { configContext, internationalizationContext } from "../../data/context";
import { DialogMixin } from "../../dialogs/dialog-mixin";
import type { HomeAssistantConfig } from "../../types";
@@ -56,13 +61,16 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
dateString: string;
};
/** used to show month in calendar-date header */
@state() private _pickerMonth?: string;
/** used to show month and year in calendar-date header */
@state() private _pickerMonthYear?: string;
/** used to show year in calendar-date header */
@state() private _pickerYear?: string;
/** used for today to navigate focus in cally-calendar-date */
/**
* used for today to navigate focus in cally-calendar-date
*
* Always mirrors the day calendar-date has focused, never cleared: once this
* has held a date, rendering `undefined` over it makes cally fall back to the
* selected value and page the calendar away from where the user is.
*/
@state() private _focusDate?: string;
public connectedCallback() {
@@ -71,12 +79,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
if (this.params) {
const date = valueToDate(this.params.value);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(
this._pickerMonthYear = formatDateMonthYear(
date,
this._i18n.locale,
this._hassConfig
@@ -84,7 +87,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
this._value = this.params.value
? {
year: this._pickerYear,
year: formatDateYear(date, this._i18n.locale, this._hassConfig),
title: formatDateShort(date, this._i18n.locale, this._hassConfig),
dateString: formatISODateOnly(
date,
@@ -140,9 +143,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
slot="previous"
></ha-icon-button-prev>
<div class="heading" slot="heading">
<span class="month-year"
>${this._pickerMonth} ${this._pickerYear}</span
>
<span class="month-year">${this._pickerMonthYear}</span>
<ha-icon-button
@click=${this._setToday}
.path=${mdiCalendarToday}
@@ -167,7 +168,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
</ha-adaptive-popover>`;
}
private _valueChanged(ev: Event) {
private _valueChanged(ev: HASSDomTargetEvent<CalendarDate>) {
const dateElement = ev.target as CalendarDate;
if (dateElement.value) {
this._updateValue(dateElement.value);
@@ -185,12 +186,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
if (setFocusDay) {
this._focusDate = this._value.dateString;
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
this._pickerMonthYear = formatDateMonthYear(
date,
this._i18n.locale,
this._hassConfig
@@ -198,19 +194,11 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
}
}
private _focusChanged(ev: CustomEvent<Date>) {
const date = ev.detail;
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._focusDate = undefined;
private _focusChanged(
ev: HASSDomEvent<Date> & HASSDomTargetEvent<CalendarDate>
) {
this._pickerMonthYear = formatCallyMonthYear(ev.detail, this._i18n.locale);
this._focusDate = ev.target.focusedDate;
}
private _clear() {
@@ -7,6 +7,10 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../common/dom/fire_event";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import type { LocalizeFunc } from "../common/translations/localize";
@@ -191,7 +195,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
}
);
private _floorMoved(ev: CustomEvent<HASSDomEvents["item-moved"]>) {
private _floorMoved(ev: HASSDomEvent<HASSDomEvents["item-moved"]>) {
ev.stopPropagation();
const newIndex = ev.detail.newIndex;
const oldIndex = ev.detail.oldIndex;
@@ -215,7 +219,12 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
fireEvent(this, "value-changed", { value: newValue });
}
private async _areaDisplayChanged(ev: ValueChangedEvent<DisplayValue>) {
private async _areaDisplayChanged(
ev: ValueChangedEvent<DisplayValue> &
HASSDomCurrentTargetEvent<
HTMLElementTagNameMap["ha-items-display-editor"] & { floorId?: string }
>
) {
ev.stopPropagation();
const value = ev.detail.value;
const currentFloorId = (ev.currentTarget as any).floorId;
+2 -1
View File
@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import type {
@@ -43,7 +44,7 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
`;
}
private _valueChanged(ev: Event) {
private _valueChanged(ev: HASSDomTargetEvent<HaCheckbox>) {
fireEvent(this, "value-changed", {
value: (ev.target as HaCheckbox).checked,
});
+30
View File
@@ -0,0 +1,30 @@
import { css, html, LitElement } from "lit";
import type { TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type { HaFormDividerSchema, HaFormElement } from "./types";
@customElement("ha-form-divider")
export class HaFormDivider extends LitElement implements HaFormElement {
@property({ attribute: false }) public schema!: HaFormDividerSchema;
protected render(): TemplateResult {
return html`<hr />`;
}
static styles = css`
:host {
display: block;
}
hr {
border: none;
border-top: 1px solid var(--ha-color-border-neutral-quiet);
margin: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-divider": HaFormDivider;
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../input/ha-input";
import type { HaInput } from "../input/ha-input";
@@ -70,7 +71,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
}
}
private _handleInput(ev: InputEvent) {
private _handleInput(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
const source = ev.target as HaInput;
const rawValue = (source.value ?? "").replace(",", ".");
+5 -2
View File
@@ -2,6 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
@@ -153,7 +154,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
);
}
private _handleCheckboxChange(ev: Event) {
private _handleCheckboxChange(ev: HASSDomTargetEvent<HaCheckbox>) {
const checked = (ev.target as HaCheckbox).checked;
let value: HaFormIntegerData | undefined;
if (checked) {
@@ -178,7 +179,9 @@ export class HaFormInteger extends LitElement implements HaFormElement {
});
}
private _valueChanged(ev: InputEvent) {
private _valueChanged(
ev: InputEvent & HASSDomTargetEvent<HaInput | HTMLInputElement>
) {
const source = ev.target as HaInput | HTMLInputElement;
const rawValue = source.value;
@@ -2,6 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
import "../ha-check-list-item";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
@@ -140,7 +141,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
}
}
private _valueChanged(ev: CustomEvent): void {
private _valueChanged(ev: HASSDomTargetEvent<HaCheckbox>): void {
const { value, checked } = ev.target as HaCheckbox;
this._handleValueChanged(value, checked);
}
@@ -12,9 +12,11 @@ import type { HaDropdownSelectEvent } from "../ha-dropdown";
import "../ha-dropdown-item";
import "../ha-svg-icon";
import "./ha-form";
import "./ha-form-divider";
import type { HaForm } from "./ha-form";
import type {
HaFormDataContainer,
HaFormDividerSchema,
HaFormElement,
HaFormOptionalActionsSchema,
HaFormSchema,
@@ -22,6 +24,11 @@ import type {
const NO_ACTIONS = [];
const DIVIDER = {
name: "",
type: "divider",
} as const satisfies HaFormDividerSchema;
@customElement("ha-form-optional_actions")
export class HaFormOptionalActions extends LitElement implements HaFormElement {
@property({ attribute: false }) public localize?: LocalizeFunc;
@@ -87,7 +94,9 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
schema: readonly HaFormSchema[],
displayActions: string[]
): HaFormSchema[] =>
schema.filter((item) => displayActions.includes(item.name))
schema
.filter((item) => displayActions.includes(item.name))
.flatMap((item) => [DIVIDER, item])
);
public render(): TemplateResult {
@@ -128,6 +137,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
${
hiddenActions.length > 0
? html`
<ha-form-divider></ha-form-divider>
<ha-dropdown
@wa-select=${this._handleAddAction}
@closed=${stopPropagation}
+2 -2
View File
@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-selector/ha-selector-select";
import type {
HaFormElement,
@@ -78,7 +78,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
`;
}
private _valueChanged(ev: CustomEvent) {
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
let value: HaFormSelectData | undefined = ev.detail.value;
+2 -1
View File
@@ -2,6 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-icon-button";
import "../input/ha-input";
@@ -79,7 +80,7 @@ export class HaFormString extends LitElement implements HaFormElement {
}
}
protected _valueChanged(ev: Event): void {
protected _valueChanged(ev: HASSDomTargetEvent<HaInput>): void {
let value: string | undefined = (ev.target as HaInput).value;
if (this.data === value) {
return;
+16 -5
View File
@@ -3,15 +3,24 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-alert";
import "../ha-selector/ha-selector";
import { getHiddenFields } from "./conditions";
import type { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types";
import type {
HaFormData,
HaFormDataContainer,
HaFormElement,
HaFormSchema,
} from "./types";
type HaFormDataChangedEvent = ValueChangedEvent<HaFormData>;
type HaFormDataContainerChangedEvent = ValueChangedEvent<HaFormDataContainer>;
const LOAD_ELEMENTS = {
boolean: () => import("./ha-form-boolean"),
constant: () => import("./ha-form-constant"),
divider: () => import("./ha-form-divider"),
float: () => import("./ha-form-float"),
grid: () => import("./ha-form-grid"),
expandable: () => import("./ha-form-expandable"),
@@ -261,16 +270,18 @@ export class HaForm extends LitElement implements HaFormElement {
}
protected addValueChangedListener(element: Element | ShadowRoot) {
element.addEventListener("value-changed", (ev) => {
element.addEventListener("value-changed", (ev: Event) => {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
if (ev.target === this) return;
const changeEv = ev as
HaFormDataChangedEvent | HaFormDataContainerChangedEvent;
const newValue =
!schema.name || ("flatten" in schema && schema.flatten)
? ev.detail.value
: { [schema.name]: ev.detail.value };
? (changeEv.detail.value as HaFormDataContainer)
: { [schema.name]: changeEv.detail.value as HaFormData };
this.data = {
...this.data,
+5
View File
@@ -4,6 +4,7 @@ import type { HaDurationData } from "../ha-duration-input";
export type HaFormSchema =
| HaFormConstantSchema
| HaFormDividerSchema
| HaFormStringSchema
| HaFormIntegerSchema
| HaFormFloatSchema
@@ -97,6 +98,10 @@ export interface HaFormConstantSchema extends HaFormBaseSchema {
value?: string;
}
export interface HaFormDividerSchema extends HaFormBaseSchema {
type: "divider";
}
export interface HaFormIntegerSchema extends HaFormBaseSchema {
type: "integer";
default?: HaFormIntegerData;
+7 -2
View File
@@ -3,6 +3,7 @@ import type { CSSResultGroup, 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 type {
Adapter,
IPv4ConfiguredAddress,
@@ -101,7 +102,9 @@ export class HaNetwork extends LitElement {
`;
}
private _handleAutoConfigureCheckboxClick(ev: Event) {
private _handleAutoConfigureCheckboxClick(
ev: HASSDomCurrentTargetEvent<HaCheckbox>
) {
const checkbox = ev.currentTarget as HaCheckbox;
if (this.networkConfig === undefined) {
return;
@@ -127,7 +130,9 @@ export class HaNetwork extends LitElement {
});
}
private _handleAdapterCheckboxClick(ev: Event) {
private _handleAdapterCheckboxClick(
ev: HASSDomCurrentTargetEvent<HaCheckbox>
) {
const checkbox = ev.currentTarget as HaCheckbox;
const adapter_name = checkbox.id;
if (this.networkConfig === undefined) {
@@ -5,7 +5,7 @@ import { getAppKey } from "../data/notify_html5";
import { showPromptDialog } from "../dialogs/generic/show-dialog-box";
import type { HaSwitch } from "./ha-switch";
import type { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { fireEvent, type HASSDomTargetEvent } from "../common/dom/fire_event";
import "./ha-switch";
export const pushSupported =
@@ -55,7 +55,7 @@ class HaPushNotificationsToggle extends LitElement {
}
}
private _handlePushChange(ev: Event) {
private _handlePushChange(ev: HASSDomTargetEvent<HaSwitch>) {
// Somehow this is triggered on Safari on page load causing
// it to get into a loop and crash the page.
if (!pushSupported) return;
+5 -2
View File
@@ -4,6 +4,7 @@ import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../common/dom/fire_event";
import "./ha-tooltip";
export interface Segment {
@@ -120,7 +121,9 @@ class HaSegmentedBar extends LitElement {
`;
}
private _handleSegmentClick(ev: Event): void {
private _handleSegmentClick(
ev: HASSDomCurrentTargetEvent<HTMLElement>
): void {
const target = ev.currentTarget as HTMLElement;
const index = Number(target.dataset.index);
const segment = this.segments[index];
@@ -129,7 +132,7 @@ class HaSegmentedBar extends LitElement {
}
}
private _handleLegendClick(ev: Event): void {
private _handleLegendClick(ev: HASSDomCurrentTargetEvent<HTMLElement>): void {
const target = ev.currentTarget as HTMLElement;
const index = Number(target.dataset.index);
const segment = this.segments[index];
+2 -1
View File
@@ -4,6 +4,7 @@ import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-dropdown";
import type { HaDropdownSelectEvent } from "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-input-helper-text";
import "./ha-picker-field";
@@ -169,7 +170,7 @@ export class HaSelect extends LitElement {
: nothing;
}
private _handleSelect(ev: CustomEvent<{ item: { value: string | number } }>) {
private _handleSelect(ev: HaDropdownSelectEvent<string | number>) {
ev.stopPropagation();
const value = ev.detail.item.value;
if (value === this.value) {
@@ -8,7 +8,6 @@ import { fireEvent } from "../../common/dom/fire_event";
import type { ConfigEntry } from "../../data/config_entries";
import { getConfigEntries } from "../../data/config_entries";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { EntitySelector } from "../../data/selector";
@@ -42,10 +41,6 @@ export class HaEntitySelector extends LitElement {
@property({ type: Boolean }) public required = true;
@property({ attribute: false }) public context?: {
entityFilter?: HaEntityPickerEntityFilterFunc;
};
@state() private _createDomains: string[] | undefined;
private _deviceIntegrationLookup = memoizeOne(
@@ -174,9 +169,6 @@ export class HaEntitySelector extends LitElement {
}
private _filterEntities = (entity: HassEntity): boolean => {
if (this.context?.entityFilter && !this.context.entityFilter(entity)) {
return false;
}
if (!this.selector?.entity?.filter) {
return true;
}
@@ -187,6 +187,7 @@ export class HaSelectSelector extends LitElement {
.idx=${idx}
@remove=${this._removeItem}
.label=${label}
.title=${label}
selected
>
${
+3 -3
View File
@@ -17,7 +17,7 @@ import { stringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import { throttle } from "../common/util/throttle";
import { subscribeFrontendUserData } from "../data/frontend";
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
import type { ActionHandlerEvent } from "../data/lovelace/action_handler";
import {
FIXED_PANELS,
getDefaultPanelUrlPath,
@@ -634,7 +634,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
});
}
private _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
private _handleAction(ev: ActionHandlerEvent) {
if (ev.detail.action !== "hold") {
return;
}
@@ -646,7 +646,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
fireEvent(this, "hass-show-notifications");
}
private _toggleSidebar(ev: CustomEvent) {
private _toggleSidebar(ev: ActionHandlerEvent) {
if (ev.detail.action !== "tap") {
return;
}
+4 -1
View File
@@ -2,6 +2,7 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { HASSDomCurrentTargetEvent } from "../common/dom/fire_event";
import { goBack } from "../common/navigate";
import { haStyleScrollbar } from "../resources/styles";
import "./ha-icon-button-arrow-prev";
@@ -324,7 +325,9 @@ export class HaTopAppBarFixed extends LitElement {
this._subRowResizeObserver = undefined;
}
private _subRowSlotChanged = (ev: Event) => {
private _subRowSlotChanged = (
ev: HASSDomCurrentTargetEvent<HTMLSlotElement>
) => {
const slot = ev.currentTarget as HTMLSlotElement;
this._hasSubRow = slot
.assignedNodes({ flatten: true })
+2 -1
View File
@@ -2,6 +2,7 @@ import { consume, type ContextType } from "@lit/context";
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
import { copyToClipboard } from "../../common/util/copy-clipboard";
import { internationalizationContext } from "../../data/context";
import { showToast } from "../../util/toast";
@@ -111,7 +112,7 @@ export class HaInputCopy extends LitElement {
`;
}
private _focusInput(ev: Event) {
private _focusInput(ev: HASSDomCurrentTargetEvent<HaInput>) {
const inputElement = ev.currentTarget as HaInput;
inputElement.select();
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
/**
* @element ha-row-item
@@ -56,7 +57,7 @@ export class HaRowItem extends LitElement {
@state() private _hasEnd = false;
private _onSlotChange(name: "start" | "end") {
return (ev: Event) => {
return (ev: HASSDomTargetEvent<HTMLSlotElement>) => {
const slot = ev.target as HTMLSlotElement;
const hasContent = slot
.assignedNodes({ flatten: true })
+45
View File
@@ -175,6 +175,8 @@ export class HaMap extends ReactiveElement {
private _pauseAutoFit = false;
private _pendingFit?: () => void;
public connectedCallback(): void {
this._pauseAutoFit = false;
document.addEventListener("visibilitychange", this._handleVisibilityChange);
@@ -204,6 +206,7 @@ export class HaMap extends ReactiveElement {
this.Leaflet = undefined;
}
this._pendingFit = undefined;
this._loaded = false;
if (this._resizeObserver) {
@@ -347,6 +350,10 @@ export class HaMap extends ReactiveElement {
return;
}
if (this._deferIfUnsized(() => this.fitMap(options))) {
return;
}
if (
!this._mapFocusItems.length &&
!this._mapFocusZones.length &&
@@ -387,6 +394,40 @@ export class HaMap extends ReactiveElement {
}, PROGRAMMITIC_FIT_DELAY);
}
// Leaflet derives the zoom level that fits given bounds from the current
// size of the map container. When the container has not been laid out yet,
// that size is 0x0 and the computed zoom collapses to the minimum, leaving
// the map zoomed out to the world even after the container gets its size.
// Defer fitting until the resize observer reports a usable size.
private _deferIfUnsized(fit: () => void): boolean {
const size = this.leafletMap!.getSize();
if (size.x > 0 && size.y > 0) {
this._pendingFit = undefined;
return false;
}
const container = this.leafletMap!.getContainer();
if (container.clientWidth > 0 && container.clientHeight > 0) {
// The container was laid out since Leaflet last measured it.
this.leafletMap!.invalidateSize(false);
this._pendingFit = undefined;
return false;
}
this._pendingFit = fit;
return true;
}
private _runPendingFit(): void {
if (!this._pendingFit || !this.leafletMap) {
return;
}
const size = this.leafletMap.getSize();
if (size.x > 0 && size.y > 0) {
const pendingFit = this._pendingFit;
this._pendingFit = undefined;
pendingFit();
}
}
public fitBounds(
boundingbox: LatLngExpression[],
options?: { zoom?: number; pad?: number }
@@ -394,6 +435,9 @@ export class HaMap extends ReactiveElement {
if (!this.leafletMap || !this.Leaflet) {
return;
}
if (this._deferIfUnsized(() => this.fitBounds(boundingbox, options))) {
return;
}
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
options?.pad ?? 0.5
);
@@ -773,6 +817,7 @@ export class HaMap extends ReactiveElement {
if (!this._resizeObserver) {
this._resizeObserver = new ResizeObserver(() => {
this.leafletMap?.invalidateSize({ debounceMoveend: true });
this._runPendingFit();
});
}
this._resizeObserver.observe(this);
+2 -1
View File
@@ -1,6 +1,7 @@
import { css, html, LitElement, nothing, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const";
interface BranchConfig {
@@ -31,7 +32,7 @@ export class HatGraphBranch extends LitElement {
private _maxHeight = 0;
private _updateBranches(ev: Event) {
private _updateBranches(ev: HASSDomTargetEvent<HTMLSlotElement>) {
let total_width = 0;
const heights: number[] = [];
const branches: BranchConfig[] = [];
+1 -1
View File
@@ -14,7 +14,7 @@ declare global {
}
interface GlobalEventHandlersEventMap {
"connection-status": HASSDomEvent<ConnectionStatus>;
"connection-status": HASSDomEvent<HASSDomEvents["connection-status"]>;
}
}
+1 -1
View File
@@ -207,7 +207,7 @@ declare global {
"hass-related-context": RelatedContextItem | undefined;
}
interface HTMLElementEventMap {
"hass-related-context": HASSDomEvent<RelatedContextItem | undefined>;
"hass-related-context": HASSDomEvent<HASSDomEvents["hass-related-context"]>;
}
}
+14 -2
View File
@@ -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"
-13
View File
@@ -1,5 +1,4 @@
import type { Connection } from "home-assistant-js-websocket";
import type { Condition } from "../panels/lovelace/common/validate-condition";
import type { ShortcutItem } from "./home_shortcuts";
export interface SurveyInteraction {
@@ -36,17 +35,6 @@ export interface HomeFrontendSystemData {
shortcuts?: ShortcutItem[];
}
export interface SecurityAlertEntityConfig {
entity: string;
color?: string;
pulse?: boolean;
visibility?: Condition[];
}
export interface SecurityFrontendSystemData {
alert_entities?: SecurityAlertEntityConfig[];
}
export interface EnergyFrontendSystemData {
// Stable "<view>.<card-type>" keys of energy dashboard cards the user has
// hidden. An absent key or array means nothing is hidden (all cards visible),
@@ -63,7 +51,6 @@ declare global {
core: CoreFrontendSystemData;
home: HomeFrontendSystemData;
energy: EnergyFrontendSystemData;
security: SecurityFrontendSystemData;
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ declare global {
}
interface GlobalEventHandlersEventMap {
haptic: HASSDomEvent<HapticType>;
haptic: HASSDomEvent<HASSDomEvents["haptic"]>;
}
}
@@ -2,6 +2,7 @@ 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 "../../components/ha-dialog";
import "../../components/ha-dialog-footer";
import "../../components/ha-formfield";
@@ -161,18 +162,18 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
`;
}
private _disableNewEntitiesChanged(ev: Event): void {
private _disableNewEntitiesChanged(ev: HASSDomTargetEvent<HaSwitch>): void {
this._error = undefined;
this._disableNewEntities = !(ev.target as HaSwitch).checked;
this._disableNewEntities = !ev.target.checked;
this._updateDirtyState({
disableNewEntities: this._disableNewEntities,
disablePolling: this._disablePolling,
});
}
private _disablePollingChanged(ev: Event): void {
private _disablePollingChanged(ev: HASSDomTargetEvent<HaSwitch>): void {
this._error = undefined;
this._disablePolling = !(ev.target as HaSwitch).checked;
this._disablePolling = !ev.target.checked;
this._updateDirtyState({
disableNewEntities: this._disableNewEntities,
disablePolling: this._disablePolling,
@@ -70,8 +70,10 @@ declare global {
}
// for add event listener
interface HTMLElementEventMap {
"flow-update": HASSDomEvent<FlowUpdateEvent>;
"flow-step-footer-state-changed": HASSDomEvent<FlowStepFooterStateChangedEvent>;
"flow-update": HASSDomEvent<HASSDomEvents["flow-update"]>;
"flow-step-footer-state-changed": HASSDomEvent<
HASSDomEvents["flow-step-footer-state-changed"]
>;
}
}
@@ -741,7 +743,7 @@ class DataEntryFlowDialog extends DirtyStateProviderMixin<
};
private _handleFooterStateChanged = (
ev: HASSDomEvent<FlowStepFooterStateChangedEvent>
ev: HASSDomEvent<HASSDomEvents["flow-step-footer-state-changed"]>
) => {
if (ev.detail.loading !== undefined) {
this._formStepLoading = ev.detail.loading;
+2 -2
View File
@@ -5,7 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { createRef, ref } from "lit/directives/ref";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import { isNavigationClick } from "../../common/dom/is-navigation-click";
import "../../components/ha-alert";
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
@@ -153,7 +153,7 @@ class StepFlowForm extends LitElement {
`;
}
private _setError(ev: CustomEvent) {
private _setError(ev: HASSDomEvent<DataEntryFlowStepForm["errors"]>) {
this._previewErrors = ev.detail;
}
@@ -2,9 +2,11 @@ import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import { slugify } from "../../../common/string/slugify";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
import "../../../components/ha-camera-stream";
import type { CameraEntity } from "../../../data/camera";
import { apiContext } from "../../../data/context";
@@ -66,8 +68,10 @@ class MoreInfoCamera extends LitElement {
`;
}
private async _downloadSnapshot(ev: CustomEvent) {
const button = ev.currentTarget as any;
private async _downloadSnapshot(
ev: HASSDomCurrentTargetEvent<HaProgressButton>
) {
const button = ev.currentTarget;
this._waiting = true;
try {
@@ -3,8 +3,10 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-button";
import type { HaButton } from "../../../components/ha-button";
import { apiContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistantApi } from "../../../types";
@@ -67,8 +69,10 @@ class MoreInfoCounter extends LitElement {
`;
}
private _handleActionClick(e: MouseEvent): void {
const action = (e.currentTarget as any).action;
private _handleActionClick(
e: MouseEvent & HASSDomCurrentTargetEvent<HaButton & { action: string }>
): void {
const action = e.currentTarget.action;
this._api.callService("counter", action, {
entity_id: this.stateObj!.entity_id,
});
@@ -17,6 +17,7 @@ import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
@@ -872,12 +873,14 @@ class MoreInfoMediaPlayer extends LitElement {
});
}
private async _handleMediaSeekChanged(e: Event): Promise<void> {
private async _handleMediaSeekChanged(
e: HASSDomTargetEvent<HaSlider>
): Promise<void> {
if (!this.stateObj) {
return;
}
const newValue = (e.target as any).value;
const newValue = e.target.value;
this._api.callService("media_player", "media_seek", {
entity_id: this.stateObj.entity_id,
seek_position: newValue,
@@ -19,7 +19,7 @@ import {
hasRequiredScriptFields,
requiredScriptFieldsFilled,
} from "../../../data/script";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import "../components/ha-more-info-state-header";
@customElement("more-info-script")
@@ -209,7 +209,9 @@ class MoreInfoScript extends LitElement {
});
}
private _scriptDataChanged(ev: CustomEvent): void {
private _scriptDataChanged(
ev: ValueChangedEvent<Record<string, unknown>>
): void {
this._scriptData = { ...this._scriptData, ...ev.detail.value };
}
@@ -3,8 +3,10 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-button";
import type { HaButton } from "../../../components/ha-button";
import "../../../components/ha-duration-input";
import type { HaDurationData } from "../../../components/ha-duration-input";
import { apiContext } from "../../../data/context";
@@ -137,8 +139,10 @@ class MoreInfoTimer extends LitElement {
});
}
private _handleActionClick(e: MouseEvent): void {
const action = (e.currentTarget as any).action;
private _handleActionClick(
e: MouseEvent & HASSDomCurrentTargetEvent<HaButton & { action: string }>
): void {
const action = e.currentTarget.action;
this._api.callService("timer", action, {
entity_id: this.stateObj!.entity_id,
});
+6 -10
View File
@@ -40,7 +40,7 @@ import {
getEntityEntryContext,
} from "../../common/entity/context/get_entity_context";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate";
import { navigate, updateHistoryState } from "../../common/navigate";
import type { LocalizeKeys } from "../../common/translations/localize";
import { computeRTL } from "../../common/util/compute_rtl";
import { withViewTransition } from "../../common/util/view-transition";
@@ -268,16 +268,12 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
}
private _setView(view: MoreInfoView) {
history.replaceState(
{
...history.state,
dialogParams: {
...history.state?.dialogParams,
view,
},
updateHistoryState({
dialogParams: {
...history.state?.dialogParams,
view,
},
""
);
});
this._currView = view;
}
+10 -4
View File
@@ -10,6 +10,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
import "../../components/animation/ha-fade-in";
import "../../components/ha-adaptive-dialog";
import "../../components/ha-alert";
@@ -334,13 +335,18 @@ class DialogRestart extends LitElement {
}
};
private async _handleAction(ev: Event) {
private async _handleAction(
ev: HASSDomCurrentTargetEvent<
HaListItemButton & {
action: "restart" | "reboot" | "shutdown" | "restart-safe-mode";
}
>
) {
if (this._loadingBackupInfo) {
return;
}
this._loadingBackupInfo = true;
const action = (ev.currentTarget as HaListItemButton & { action: string })
.action as "restart" | "reboot" | "shutdown" | "restart-safe-mode";
const action = ev.currentTarget.action;
const backupState = await this._loadBackupState();
@@ -369,7 +375,7 @@ class DialogRestart extends LitElement {
this._dialogOpen = false;
let actionFunc;
let actionFunc: () => Promise<void>;
if (["restart", "restart-safe-mode"].includes(action)) {
const serviceData =
@@ -3,7 +3,7 @@ import type { CSSResultGroup, PropertyValues } from "lit";
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 { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { formatLanguageCode } from "../../common/language/format_language";
import "../../components/chips/ha-assist-chip";
@@ -18,7 +18,7 @@ import { getLanguageScores } from "../../data/conversation";
import { UNAVAILABLE } from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import type { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog";
import "./voice-assistant-setup-step-area";
import "./voice-assistant-setup-step-change-wake-word";
@@ -337,7 +337,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
this._language = ev.detail.item.value;
}
private _languageChanged(ev: CustomEvent) {
private _languageChanged(ev: ValueChangedEvent<string | undefined>) {
if (!ev.detail.value) {
return;
}
@@ -351,7 +351,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
this._step = this._previousSteps.pop()!;
}
private async _goToNextStep(ev?: CustomEvent) {
private async _goToNextStep(ev?: HASSDomEvent<HASSDomEvents["next-step"]>) {
if (ev?.detail?.updateConfig) {
await this._fetchAssistConfiguration();
}
@@ -1,6 +1,7 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
import "../../components/item/ha-list-item-button";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
@@ -50,14 +51,14 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
</ha-list-base>`;
}
private async _wakeWordPicked(ev: Event) {
private async _wakeWordPicked(
ev: HASSDomCurrentTargetEvent<HaListItemButton & { value: string }>
) {
if (!this.assistEntityId) {
return;
}
const wakeWordId = (
ev.currentTarget as HaListItemButton & { value: string }
).value;
const wakeWordId = ev.currentTarget.value;
await setWakeWords(this.hass, this.assistEntityId, [wakeWordId]);
this._nextStep();
@@ -19,7 +19,7 @@ import type { LanguageScore, LanguageScores } from "../../data/conversation";
import { getLanguageScores, listAgents } from "../../data/conversation";
import { listSTTEngines } from "../../data/stt";
import { listTTSEngines, listTTSVoices } from "../../data/tts";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { AssistantSetupStyles } from "./styles";
import { STEP } from "./voice-assistant-setup-dialog";
@@ -265,8 +265,8 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
}
private async _createCloudPipeline(useLanguage: boolean): Promise<boolean> {
let cloudTtsEntityId;
let cloudSttEntityId;
let cloudTtsEntityId: string | undefined;
let cloudSttEntityId: string | undefined;
for (const entity of Object.values(this.hass.entities)) {
if (entity.platform === "cloud") {
const domain = computeDomain(entity.entity_id);
@@ -327,7 +327,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
const ttsVoices = await listTTSVoices(
this.hass,
cloudTtsEntityId,
cloudTtsEntityId!,
ttsEngine.supported_languages[0]
);
@@ -357,9 +357,9 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
language: (this.language || this.hass.config.language).split("-")[0],
conversation_engine: "conversation.home_assistant",
conversation_language: agent.supported_languages[0],
stt_engine: cloudSttEntityId,
stt_engine: cloudSttEntityId!,
stt_language: sttEngine.supported_languages[0],
tts_engine: cloudTtsEntityId,
tts_engine: cloudTtsEntityId!,
tts_language: ttsEngine.supported_languages[0],
tts_voice: ttsVoices.voices![0].voice_id,
wake_word_entity: null,
@@ -380,7 +380,9 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
}
}
private _valueChanged(ev: CustomEvent) {
private _valueChanged(
ev: ValueChangedEvent<(typeof OPTIONS)[number] | undefined>
) {
this._value = ev.detail.value;
}
@@ -410,7 +412,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
fireEvent(this, "next-step", { step: STEP.LOCAL, option: this._value });
}
private _languageChanged(ev: CustomEvent) {
private _languageChanged(ev: ValueChangedEvent<string | undefined>) {
if (!ev.detail.value) {
return;
}
+13
View File
@@ -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);
}
});
+9 -1
View File
@@ -121,6 +121,14 @@ declare global {
}
interface GlobalEventHandlersEventMap {
"matter-commission-finish": HASSDomEvent<MatterCommissionFinish>;
"improv-discovered-device": HASSDomEvent<
HASSDomEvents["improv-discovered-device"]
>;
"improv-device-setup-done": HASSDomEvent<
HASSDomEvents["improv-device-setup-done"]
>;
"matter-commission-finish": HASSDomEvent<
HASSDomEvents["matter-commission-finish"]
>;
}
}
+21 -15
View File
@@ -3,6 +3,8 @@ import { css, html, LitElement } from "lit";
import { customElement, eventOptions, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { restoreScroll } from "../common/decorators/restore-scroll";
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { goBack } from "../common/navigate";
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
import "../components/ha-icon-button-arrow-prev";
@@ -36,19 +38,14 @@ class HassSubpage extends LitElement {
<div class="toolbar ${classMap({ narrow: this.narrow })}">
<div class="toolbar-content">
${
this.mainPage || history.state?.root
this.mainPage || (!backPath && history.state?.root)
? html`<ha-menu-button></ha-menu-button>`
: backPath
? html`
<ha-icon-button-arrow-prev
href=${backPath}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
.href=${backPath}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
}
<div class="main-title">
@@ -74,16 +71,25 @@ class HassSubpage extends LitElement {
}
@eventOptions({ passive: true })
private _saveScrollPos(e: Event) {
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
}
private _backTapped(): void {
private _backTapped(ev: MouseEvent): void {
const backPath = sanitizeNavigationPath(this.backPath);
// Middle click, ctrl and cmd open the parent in a new tab, let the anchor
// handle those.
if (backPath && !isNavigationClick(ev)) {
return;
}
if (this.backCallback) {
this.backCallback();
return;
}
goBack();
goBack(backPath);
}
static get styles(): CSSResultGroup {
+4 -2
View File
@@ -17,7 +17,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { canShowPage } from "../common/config/can_show_page";
import { fireEvent } from "../common/dom/fire_event";
import { fireEvent, type HASSDomTargetEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/chips/ha-assist-chip";
import "../components/data-table/ha-data-table";
@@ -740,7 +740,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
this._dataTable.clearSelection();
};
private _handleSearchChange(ev: InputEvent) {
private _handleSearchChange(
ev: InputEvent & HASSDomTargetEvent<HaInputSearch>
) {
const target = ev.target as HaInputSearch;
if (this.filter === target.value) {
return;
+19 -14
View File
@@ -12,6 +12,7 @@ import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { canShowPage } from "../common/config/can_show_page";
import { restoreScroll } from "../common/decorators/restore-scroll";
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { goBack, navigate } from "../common/navigate";
import type { LocalizeFunc } from "../common/translations/localize";
@@ -174,17 +175,12 @@ export class HassTabsSubpage extends LitElement {
${
this.mainPage || (!backPath && history.state?.root)
? html`<ha-menu-button></ha-menu-button>`
: backPath
? html`
<ha-icon-button-arrow-prev
.href=${backPath}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
.href=${backPath}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
}
${
this._narrow || !this.showTabs
@@ -232,7 +228,7 @@ export class HassTabsSubpage extends LitElement {
}
@eventOptions({ passive: true })
private _saveScrollPos(e: Event) {
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
}
@@ -245,12 +241,21 @@ export class HassTabsSubpage extends LitElement {
this._content.focus({ preventScroll: true });
}
private _backTapped(): void {
private _backTapped(ev: MouseEvent): void {
const backPath = sanitizeNavigationPath(this.backPath);
// Middle click, ctrl and cmd open the parent in a new tab, let the anchor
// handle those.
if (backPath && !isNavigationClick(ev)) {
return;
}
if (this.backCallback) {
this.backCallback();
return;
}
goBack();
goBack(backPath);
}
private _isActiveTabPath(tabPath: string, currentPath: string): boolean {
+1 -1
View File
@@ -36,7 +36,7 @@ export const PreventUnsavedMixin = <T extends Constructor<LitElement>>(
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (this.isDirtyState) {
if (this.isDirtyState && this.isConnected) {
window.addEventListener("click", this._handleClick, true);
window.addEventListener("beforeunload", this._handleUnload);
} else {
+10 -6
View File
@@ -37,7 +37,7 @@ import { subscribeUser } from "../data/ws-user";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { HassElement } from "../state/hass-element";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import { storeState } from "../util/ha-pref-storage";
import { registerServiceWorker } from "../util/register-service-worker";
import "../components/progress/ha-progress-bar";
@@ -80,8 +80,8 @@ declare global {
}
interface GlobalEventHandlersEventMap {
"onboarding-step": HASSDomEvent<OnboardingEvent>;
"onboarding-progress": HASSDomEvent<OnboardingProgressEvent>;
"onboarding-step": HASSDomEvent<HASSDomEvents["onboarding-step"]>;
"onboarding-progress": HASSDomEvent<HASSDomEvents["onboarding-progress"]>;
}
}
@@ -332,7 +332,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
}
private _handleProgress(ev: HASSDomEvent<OnboardingProgressEvent>) {
private _handleProgress(
ev: HASSDomEvent<HASSDomEvents["onboarding-progress"]>
) {
const stepSize = 100 / this._steps!.length;
if (ev.detail.increase) {
this._progress += ev.detail.increase * stepSize;
@@ -345,7 +347,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
}
private async _handleStepDone(ev: HASSDomEvent<OnboardingEvent>) {
private async _handleStepDone(
ev: HASSDomEvent<HASSDomEvents["onboarding-step"]>
) {
const stepResult = ev.detail;
this._steps = this._steps!.map((step) =>
step.step === stepResult.type ? { ...step, done: true } : step
@@ -479,7 +483,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
});
}
private _languageChanged(ev: CustomEvent) {
private _languageChanged(ev: ValueChangedEvent<string>) {
const language = ev.detail.value;
this.language = language;
if (this.hass) {
+4 -2
View File
@@ -2,7 +2,7 @@ import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { fireEvent, type HASSDomEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-analytics";
import "../components/ha-button";
@@ -65,7 +65,9 @@ class OnboardingAnalytics extends LitElement {
});
}
private _preferencesChanged(event: CustomEvent): void {
private _preferencesChanged(
event: HASSDomEvent<HASSDomEvents["analytics-preferences-changed"]>
): void {
this._analyticsDetails = {
...this._analyticsDetails!,
preferences: event.detail.preferences,
+4 -2
View File
@@ -19,7 +19,7 @@ import type { ConfigUpdateValues } from "../data/core";
import { saveCoreConfig } from "../data/core";
import { countryCurrency } from "../data/currency";
import { onboardCoreConfigStep } from "../data/onboarding";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import { getLocalLanguage } from "../util/common-translation";
import "./onboarding-location";
@@ -132,7 +132,9 @@ class OnboardingCoreConfig extends LitElement {
});
}
private _handleFormChanged(ev: CustomEvent) {
private _handleFormChanged(
ev: ValueChangedEvent<{ country?: string; time_zone?: string }>
) {
const value = ev.detail.value as { country?: string; time_zone?: string };
this._country = value.country || undefined;
if (value.time_zone) {
+4 -1
View File
@@ -2,6 +2,7 @@ import type { TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { storage } from "../common/decorators/storage";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate";
import type { LocalizeFunc } from "../common/translations/localize";
import { removeSearchParam } from "../common/url/search-params";
@@ -280,7 +281,9 @@ class OnboardingRestoreBackup extends LitElement {
setTimeout(() => this._loadBackupInfo(), delay);
}
private async _backupUploaded(ev: CustomEvent) {
private async _backupUploaded(
ev: HASSDomEvent<HASSDomEvents["backup-uploaded"]>
) {
this._backupId = ev.detail.backupId;
await this._loadBackupInfo();
}
@@ -2,6 +2,10 @@ import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { formatDateTimeWithBrowserDefaults } from "../../common/datetime/format_date_time";
import { fireEvent } from "../../common/dom/fire_event";
import type {
HASSDomCurrentTargetEvent,
HASSDomTargetEvent,
} from "../../common/dom/fire_event";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
@@ -9,6 +13,7 @@ import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/input/ha-input";
import type { HaInput } from "../../components/input/ha-input";
import "../../components/item/ha-row-item";
import {
getPreferredAgentForDownload,
@@ -19,6 +24,7 @@ import { restoreOnboardingBackup } from "../../data/backup_onboarding";
import "../../panels/config/backup/components/ha-backup-data-picker";
import "../../panels/config/backup/components/ha-backup-formfield-label";
import { onBoardingStyles } from "../styles";
import type { ValueChangedEvent } from "../../types";
@customElement("onboarding-restore-backup-restore")
class OnboardingRestoreBackupRestore extends LitElement {
@@ -247,16 +253,20 @@ class OnboardingRestoreBackupRestore extends LitElement {
fireEvent(this, "sign-out");
}
private _selectedBackupChanged(ev: CustomEvent) {
private _selectedBackupChanged(ev: ValueChangedEvent<BackupData>) {
ev.stopPropagation();
this._selectedData = ev.detail.value;
}
private _encryptionKeyChanged(ev): void {
private _encryptionKeyChanged(
ev: HASSDomTargetEvent<Omit<HaInput, "value"> & { value: string }>
): void {
this._encryptionKey = ev.target.value;
}
private async _startRestore(ev: CustomEvent): Promise<void> {
private async _startRestore(
ev: HASSDomCurrentTargetEvent<HaProgressButton>
): Promise<void> {
const agentId = Object.keys(this.backup.agents)[0];
const backupProtected = this.backup.agents[agentId].protected;
@@ -270,7 +280,7 @@ class OnboardingRestoreBackupRestore extends LitElement {
}
this._loading = true;
const button = ev.currentTarget as HaProgressButton;
const button = ev.currentTarget;
this._error = undefined;
this._encryptionKeyWrong = false;
@@ -6,14 +6,20 @@ import type { ByWeekday, Options, WeekdayStr } from "rrule";
import { RRule, Weekday } from "rrule";
import { formatDate, formatTime } from "../../common/datetime/calc_date";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import type {
HASSDomCurrentTargetEvent,
HASSDomTargetEvent,
} from "../../common/dom/fire_event";
import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/chips/ha-chip-set";
import "../../components/chips/ha-filter-chip";
import type { HaFilterChip } from "../../components/chips/ha-filter-chip";
import "../../components/ha-date-input";
import "../../components/ha-select";
import type { HaSelectSelectEvent } from "../../components/ha-select";
import "../../components/input/ha-input";
import type { HomeAssistant } from "../../types";
import type { HaInput } from "../../components/input/ha-input";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import type {
MonthlyRepeatItem,
RepeatEnd,
@@ -321,8 +327,8 @@ export class RecurrenceRuleEditor extends LitElement {
`;
}
private _onIntervalChange(e: Event) {
this._interval = (e.target! as any).value;
private _onIntervalChange(e: HASSDomTargetEvent<HaInput>) {
this._interval = Number(e.target.value);
}
private _onRepeatSelected(e: HaSelectSelectEvent<RepeatFrequency>) {
@@ -351,9 +357,12 @@ export class RecurrenceRuleEditor extends LitElement {
this._monthday = selectedItem.bymonthday;
}
private _onWeekdayToggle(e: MouseEvent) {
const target = e.currentTarget as any;
const value = target.value as WeekdayStr;
private _onWeekdayToggle(
e: MouseEvent &
HASSDomCurrentTargetEvent<HaFilterChip & { value: WeekdayStr }>
) {
const target = e.currentTarget;
const value = target.value;
if (this._weekday.has(value)) {
this._weekday.delete(value);
} else {
@@ -385,11 +394,11 @@ export class RecurrenceRuleEditor extends LitElement {
e.stopPropagation();
}
private _onCountChange(e: Event) {
this._count = (e.target! as any).value;
private _onCountChange(e: HASSDomTargetEvent<HaInput>) {
this._count = Number(e.target.value);
}
private _onUntilChange(e: CustomEvent) {
private _onUntilChange(e: ValueChangedEvent<string>) {
e.stopPropagation();
this._untilDay = new Date(
new TZDate(e.detail.value + "T00:00:00", this.timezone).getTime()
@@ -436,7 +445,7 @@ export class RecurrenceRuleEditor extends LitElement {
);
// rrule.js can't compute some UNTIL variations so we compute that ourself. Must be
// in the same format as dtstart.
let newUntilValue;
let newUntilValue: string;
if (this.allDay) {
// For all-day events, only use the date part
newUntilValue = until.toISOString().split("T")[0].replace(/-/g, "");
@@ -644,6 +644,7 @@ class HaConfigAreaPage extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/areas/dashboard"
.header=${html`${
area.icon
? html`<ha-icon
@@ -86,8 +86,6 @@ export class HaConfigAreasDashboard extends LitElement {
@state() private _hierarchy?: AreasFloorHierarchy;
private _searchParms = new URLSearchParams(window.location.search);
private _blockHierarchyUpdate = false;
private _blockHierarchyUpdateTimeout?: number;
@@ -168,9 +166,7 @@ export class HaConfigAreasDashboard extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.isWide=${this.isWide}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
.tabs=${configSections.areas}
.route=${this.route}
has-fab
@@ -317,7 +317,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
color:
automation.state === UNAVAILABLE
? "var(--error-color)"
: "unset",
: automation.state === "on"
? "var(--state-active-color)"
: "unset",
})}
></ha-state-icon>`,
},
@@ -346,19 +348,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,
@@ -443,9 +443,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
id="entity_id"
.route=${this.route}
.tabs=${configSections.automations}
@@ -14,7 +14,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
@@ -110,6 +110,7 @@ export class HaAutomationTrace extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/automation/dashboard"
.header=${title}
.scrollable=${this.narrow}
>
@@ -452,11 +453,7 @@ export class HaAutomationTrace extends LitElement {
if (runId) {
const params = new URLSearchParams(location.search);
params.delete("run_id");
history.replaceState(
null,
"",
`${location.pathname}?${params.toString()}`
);
replaceCurrentUrl(`${location.pathname}?${params.toString()}`);
}
await showAlertDialog(this, {
@@ -11,6 +11,7 @@ import { property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { storage } from "../../../common/decorators/storage";
import { fireEvent } from "../../../common/dom/fire_event";
import { replaceCurrentUrl } from "../../../common/navigate";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
import {
extractSearchParam,
@@ -171,11 +172,7 @@ export const ManualEditorMixin = <TConfig>(
}
protected clearParam(param: string) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
replaceCurrentUrl(constructUrlCurrentPath(removeSearchParam(param)));
}
protected async openSidebar(ev: CustomEvent<SidebarConfig>) {
@@ -51,6 +51,8 @@ const baseConfigStruct = object({
mode: optional(string()),
max_exceeded: optional(string()),
id: optional(string()),
variables: optional(object()),
trigger_variables: optional(object()),
});
const automationConfigStruct = union([
@@ -257,6 +259,27 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
}
}
// If an object can be ambiguously an action or an automation, check for
// toplevel automation keywords to disqualify it
function isQualifiedAction(cfg): boolean {
const type = getActionType(cfg);
if (type === "variables") {
if (
[
"trigger",
"triggers",
"condition",
"conditions",
"action",
"actions",
].some((key) => key in cfg)
) {
return false;
}
}
return type !== "unknown";
}
if (Array.isArray(config)) {
if (config.length === 1) {
config = config[0];
@@ -276,7 +299,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
found = true;
(newConfig.conditions as Condition[]).push(cfg);
}
if (getActionType(cfg) !== "unknown") {
if (isQualifiedAction(cfg)) {
found = true;
(newConfig.actions as Action[]).push(cfg);
}
@@ -293,7 +316,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
if (isCondition(config)) {
config = { conditions: [config] };
}
if (getActionType(config) !== "unknown") {
if (isQualifiedAction(config)) {
config = { actions: [config] };
}
@@ -375,6 +398,11 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
if (!workingCopy) {
return;
}
["variables", "trigger_variables"].forEach((key) => {
if (key in config) {
workingCopy[key] = { ...workingCopy[key], ...config[key] };
}
});
if ("triggers" in config) {
workingCopy.triggers = ensureArray(workingCopy.triggers || []).concat(
@@ -74,8 +74,6 @@ class HaConfigBackupOverview extends LitElement {
@state() private _config?: BackupConfig;
private _searchParms = new URLSearchParams(window.location.search);
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("config") && !this._config) {
@@ -206,9 +204,7 @@ class HaConfigBackupOverview extends LitElement {
return html`
<hass-subpage
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config/system"
}
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.backup.overview.header")}
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { replaceCurrentUrl } from "../../../common/navigate";
import { debounce } from "../../../common/util/debounce";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-alert";
@@ -118,7 +119,7 @@ class HaConfigBackupSettings extends LitElement {
}
private _clearHash() {
history.replaceState(null, "", window.location.pathname);
replaceCurrentUrl(window.location.pathname);
}
protected render() {
@@ -21,6 +21,7 @@ export class CloudForgotPassword extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.header=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.title"
)}
@@ -45,6 +45,7 @@ export class CloudLoginPanel extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
header="Home Assistant Cloud"
>
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
@@ -38,6 +38,7 @@ export class CloudRegister extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.header=${this.hass.localize("ui.panel.config.cloud.register.title")}
>
<div class="content">
@@ -66,8 +66,6 @@ class HaConfigSectionUpdates extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _showSkipped = false;
@state() private _supervisorInfo?: HassioSupervisorInfo;
@@ -155,9 +153,7 @@ class HaConfigSectionUpdates extends LitElement {
return html`
<hass-subpage
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config/system"
}
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.updates.caption")}
@@ -986,6 +986,7 @@ export class HaConfigDevicePage extends LitElement {
return html`<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/devices/dashboard"
.header=${deviceName}
>
<ha-tooltip for="edit-settings-button" slot="toolbar-icon">
@@ -23,7 +23,7 @@ import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { navigate, updateHistoryState } from "../../../common/navigate";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
@@ -778,9 +778,7 @@ export class HaConfigDeviceDashboard extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
.tabs=${configSections.devices}
.route=${this.route}
.searchLabel=${this.hass.localize(
@@ -1043,7 +1041,7 @@ export class HaConfigDeviceDashboard extends LitElement {
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
history.replaceState({ filter: this._filter }, "");
updateHistoryState({ filter: this._filter });
}
private _addDevice() {
+1 -7
View File
@@ -78,8 +78,6 @@ class HaConfigEnergy extends LitElement {
@property({ attribute: false }) public route!: Route;
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _info?: EnergyInfo;
@state() private _preferences?: EnergyPreferences;
@@ -126,11 +124,7 @@ class HaConfigEnergy extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.backPath=${
this._searchParms.has("historyBack")
? undefined
: "/config/lovelace/dashboards"
}
back-path="/config/lovelace/dashboards"
.route=${this.route}
.tabs=${TABS}
>
@@ -39,6 +39,7 @@ import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { updateHistoryState } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
@@ -810,9 +811,7 @@ export class HaConfigEntities extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
.route=${this.route}
.tabs=${configSections.devices}
.columns=${this._columns(this.hass.localize, filteredEntities)}
@@ -1246,7 +1245,7 @@ export class HaConfigEntities extends LitElement {
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
history.replaceState({ filter: this._filter }, "");
updateHistoryState({ filter: this._filter });
}
private _handleSelectionChanged(
@@ -644,9 +644,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
.route=${this.route}
.tabs=${configSections.devices}
.searchLabel=${this.hass.localize(
@@ -373,7 +373,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
: this._manifest?.documentation;
return html`
<hass-subpage .hass=${this.hass} .narrow=${this.narrow}>
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/integrations/dashboard"
>
${
documentationLink
? html`

Some files were not shown because too many files have changed in this diff Show More