Compare commits

..

16 Commits

Author SHA1 Message Date
Petar Petrov 25b3d20321 Add initial value setting to input_boolean helper dialog 2026-08-05 15:48:34 +03:00
Petar Petrov f42a5012a9 Accept hostnames and localize validation messages in the HTTP config form (#53511)
Accept hostnames in HTTP config listen addresses and localize number validation
2026-08-05 12:52:52 +02:00
Petar Petrov 90f5c7a349 Only show the sidebar tab switcher when its tabs are labelled (#53508) 2026-08-05 12:31:15 +02:00
Aidan Timson d03ba15c09 Fix notification stacking (#53486)
* Stack notifications independently

* Preserve identified notifications on legacy close

* Preserve updated notifications during close

* Fix notification stack positioning

* Update src/managers/notification-manager.ts

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-08-05 11:54:11 +03:00
Petar Petrov 7143acc860 Remove orphaned for_you translation key (#53509) 2026-08-05 08:30:11 +01:00
Aidan Timson c9c46d4507 Use typed gallery demo card configs (#53492) 2026-08-05 08:26:51 +01:00
Aidan Timson 5916775745 Wait for Integrations panel readiness (#53481)
* Wait for Integrations panel readiness

* Consolidate panel readiness tests

* Address panel readiness 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-05 08:44:16 +03:00
pcan08 52ee78d8a2 Add support for climate in target humidity feature (#53502)
* Add target humidity feature to climate entity

* Use target_humidity_step if any instead of fixed step

* Add target humidity for climate in suggestd tile card
2026-08-05 08:30:45 +03:00
Aidan Timson 146a089044 Type configuration event handlers (#53460)
* Type configuration event handlers

* Fix event handler element typings

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

* Fix Prettier formatting in zha-options-page.ts and zwave_js-custom-param.ts

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

* Correction

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Correction

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Fix TS cast and formatting in dialog-energy-grid-settings

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-08-05 08:23:14 +03:00
Paul Bottein 8c566b43b6 Compress faster by running brotli in parallel (#53495) 2026-08-05 08:19:30 +03:00
larry 1089c5d1c5 Fix missing Internet heading in Home Assistant URL settings (#53498)
fix(config): always show the Internet URL heading

The "Internet" heading above the external URL field is rendered
inside a `hasCloud` conditional, so only users logged in to Home
Assistant Cloud ever see it. Everyone else gets an unlabeled URL
input, while the "Local network" heading below it always renders.
That leaves the local heading as the only label on the card, and it
sits below the external field, so the external field reads as though
it belongs to the local section.

Move the heading out of the conditional so it renders for every
user, leaving `hasCloud` to gate only the "Use Home Assistant Cloud"
toggle. The external section now has the same structure as the local
one: a heading followed by its list item.

The label was shown to non-Cloud users before #22379, inline beside
the field rather than as a heading. That PR converted both labels to
headings, kept the Cloud branch and dropped the non-Cloud one.


Claude-Session: https://claude.ai/code/session_01GQCiHCdQ4p3cPxXG6XNAa9

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 08:16:35 +03:00
Petar Petrov 2894113033 Keep numeric slider and buttons within min and max when the step overshoots (#53489)
* Clamp numeric controls after snapping to the step

* Cover the pointer path, display and arrow keys at the bounds
2026-08-05 08:02:58 +03:00
Petar Petrov b9bbef3bfc Add expand_legend option to energy graph cards (#53476) 2026-08-05 08:02:28 +03:00
Paul Bottein 706382cb68 Fix back navigation loop on the cloud page (#53497) 2026-08-05 06:50:34 +02:00
karwosts cc17921c22 Fix missing label in solar forecast dialog (#53506) 2026-08-05 06:49:42 +02:00
renovate[bot] b5ef563b71 Update octokit monorepo (#53505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-05 06:43:34 +02:00
148 changed files with 3667 additions and 2363 deletions
+46
View File
@@ -0,0 +1,46 @@
// Gulp transform that brotli-compresses files, several at a time.
//
// Drop-in replacement for gulp-brotli. zlib already does the work off the main
// thread, but that plugin wraps it in through2, which waits for each file
// before starting the next, so only one compression is ever in flight. The
// compressed bytes are unchanged; only how many run at once differs.
//
// The real ceiling is libuv's threadpool, which zlib runs on. It sizes itself
// from UV_THREADPOOL_SIZE before any JavaScript runs, so it can only be raised
// from the environment, never from inside the build.
import { availableParallelism } from "node:os";
import { buffer as readStream } from "node:stream/consumers";
import { promisify } from "node:util";
import { brotliCompress } from "node:zlib";
import { ParallelTransform } from "./parallel-transform.mjs";
const EXTENSION = ".br";
const compress = promisify(brotliCompress);
/**
* @param {object} [options]
* @param {boolean} [options.skipLarger] Drop files that compression grows.
* @param {object} [options.params] Brotli parameters, passed to zlib as-is.
*/
export default ({ skipLarger = false, params } = {}) =>
new ParallelTransform(availableParallelism(), async (file) => {
if (file.isNull()) {
return file;
}
if (file.isStream()) {
file.contents = await readStream(file.contents);
}
const compressed = await compress(file.contents, { params });
if (skipLarger && compressed.length >= file.contents.length) {
// Dropped rather than passed through, as gulp-brotli did: the
// uncompressed file is already in the output directory.
return undefined;
}
file.contents = compressed;
file.path += EXTENSION;
return file;
});
+1 -1
View File
@@ -2,7 +2,7 @@
import { constants } from "node:zlib";
import gulp from "gulp";
import brotli from "gulp-brotli";
import brotli from "../brotli.mjs";
import paths from "../paths.cjs";
import zopfli from "../zopfli.mjs";
+64
View File
@@ -0,0 +1,64 @@
// Object-mode transform that keeps several files in flight at once.
//
// through2 and node's Transform both wait for the previous callback before
// handling the next file, which serialises asynchronous work down to one file
// at a time. This keeps `limit` files in flight and applies backpressure beyond
// that. Files are emitted in completion order rather than input order.
import { Transform } from "node:stream";
export 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;
}
}
}
+1 -60
View File
@@ -6,9 +6,9 @@
// 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";
import { ParallelTransform } from "./parallel-transform.mjs";
const WORKER_URL = new URL("./zopfli-worker.mjs", import.meta.url);
const EXTENSION = ".gz";
@@ -112,65 +112,6 @@ const sharedPool = () => {
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.
+1 -31
View File
@@ -17,9 +17,6 @@ const rspackConfigPath = fileURLToPath(
new URL("./rspack.config.cjs", import.meta.url)
);
// Applies everywhere, including the files exempted from the history rule below.
const restrictedSyntax = ["LabeledStatement", "WithStatement"];
export default tseslint.config(
js.configs.recommended,
eslintConfigPrettier,
@@ -114,16 +111,7 @@ export default tseslint.config(
"no-bitwise": "error",
"no-console": "error",
"no-restricted-globals": [2, "event"],
"no-restricted-syntax": [
"error",
...restrictedSyntax,
{
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.",
},
],
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"wc/no-self-class": "off",
// import-x rules
@@ -234,24 +222,6 @@ 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", ...restrictedSyntax],
},
},
{
files: ["src/util/recorder-worklet.js"],
languageOptions: {
@@ -0,0 +1,7 @@
import type { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import { getCardElementClass } from "../../../src/panels/lovelace/create-element/create-card-element";
export const validateCardConfig = async (config: LovelaceCardConfig) => {
const cardClass = await getCardElementClass(config.type);
new cardClass().setConfig(config);
};
+40 -9
View File
@@ -1,15 +1,21 @@
import { load } from "js-yaml";
import { dump } from "js-yaml";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/components/ha-alert";
import type { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import "../../../src/panels/lovelace/cards/hui-card";
import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card";
import type { HomeAssistant } from "../../../src/types";
import { validateCardConfig } from "../common/validate-card-config";
export interface DemoCardConfig {
export interface DemoCardConfig<
T extends LovelaceCardConfig = LovelaceCardConfig,
> {
heading: string;
config: string;
config: T;
expectConfigError?: boolean;
}
@customElement("demo-card")
@@ -23,12 +29,29 @@ class DemoCard extends LitElement {
@state() private _size?: number;
@state() private _configError?: string;
@query("hui-card", false) private _card?: HuiCard;
private _config = memoizeOne((config: string) => {
const c = (load(config) as any)[0];
return c;
});
private _yamlConfig = memoizeOne((config: LovelaceCardConfig) =>
dump([config]).trim()
);
protected async firstUpdated() {
try {
await validateCardConfig(this.config.config);
} catch (err) {
if (this.config.expectConfigError) {
return;
}
this._configError = err instanceof Error ? err.message : String(err);
return;
}
if (this.config.expectConfigError) {
this._configError = `Expected config error for ${this.config.heading}`;
}
}
render() {
return html`
@@ -40,15 +63,20 @@ class DemoCard extends LitElement {
: ""
}
</h2>
${
this._configError
? html`<ha-alert alert-type="error">${this._configError}</ha-alert>`
: nothing
}
<div class="root">
<hui-card
.config=${this._config(this.config.config)}
.config=${this.config.config}
.hass=${this.hass}
@card-updated=${this._cardUpdated}
></hui-card>
${
this.showConfig
? html`<pre>${this.config.config.trim()}</pre>`
? html`<pre>${this._yamlConfig(this.config.config)}</pre>`
: nothing
}
</div>
@@ -81,6 +109,9 @@ class DemoCard extends LitElement {
font-size: 0.5em;
color: var(--primary-text-color);
}
ha-alert {
margin-bottom: 16px;
}
hui-card {
max-width: 400px;
width: 100vw;
+30 -30
View File
@@ -1,6 +1,8 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { AlarmPanelCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -40,52 +42,50 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Basic Example",
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm
`,
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm",
},
},
{
heading: "With Title",
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm_armed
name: My Alarm
`,
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm_armed",
name: "My Alarm",
},
},
{
heading: "Code Example",
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm_code
`,
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm_code",
},
},
{
heading: "Using only Arm_Home State",
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm
states:
- arm_home
`,
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm",
states: ["arm_home"],
},
},
{
heading: "Unavailable",
config: `
- type: alarm-panel
entity: alarm_control_panel.unavailable
states:
- arm_home
`,
config: {
type: "alarm-panel",
entity: "alarm_control_panel.unavailable",
states: ["arm_home"],
},
},
{
heading: "Invalid Entity",
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm1
`,
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm1",
},
},
];
] satisfies DemoCardConfig<AlarmPanelCardConfig>[];
@customElement("demo-lovelace-alarm-panel-card")
class DemoAlarmPanelEntity extends LitElement {
+19 -17
View File
@@ -1,6 +1,8 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { AreaCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -80,33 +82,33 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Bedroom",
config: `
- type: area
area: bedroom
`,
config: {
type: "area",
area: "bedroom",
},
},
{
heading: "Living Room",
config: `
- type: area
area: living_room
`,
config: {
type: "area",
area: "living_room",
},
},
{
heading: "Office",
config: `
- type: area
area: office
`,
config: {
type: "area",
area: "office",
},
},
{
heading: "Kitchen",
config: `
- type: area
area: kitchen
`,
config: {
type: "area",
area: "kitchen",
},
},
];
] satisfies DemoCardConfig<AreaCardConfig>[];
@customElement("demo-lovelace-area-card")
class DemoArea extends LitElement {
+32 -25
View File
@@ -1,7 +1,12 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
ConditionalCardConfig,
EntitiesCardConfig,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -39,35 +44,37 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Controller",
config: `
- type: entities
entities:
- light.controller_1
- light.controller_2
- type: divider
- light.floor
- light.kitchen
`,
config: {
type: "entities",
entities: [
"light.controller_1",
"light.controller_2",
{ type: "divider" },
"light.floor",
"light.kitchen",
],
},
},
{
heading: "Demo",
config: `
- type: conditional
conditions:
- entity: light.controller_1
state: "on"
- entity: light.controller_2
state_not: "off"
card:
type: entities
entities:
- light.controller_1
- light.controller_2
- light.floor
- light.kitchen
`,
config: {
type: "conditional",
conditions: [
{ entity: "light.controller_1", state: "on" },
{ entity: "light.controller_2", state_not: "off" },
],
card: {
type: "entities",
entities: [
"light.controller_1",
"light.controller_2",
"light.floor",
"light.kitchen",
],
},
},
},
];
] satisfies DemoCardConfig<EntitiesCardConfig | ConditionalCardConfig>[];
@customElement("demo-lovelace-conditional-card")
class DemoConditional extends LitElement {
+166 -135
View File
@@ -1,7 +1,13 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
EntitiesCardConfig,
EntitiesCardEntityConfig,
} from "../../../../src/panels/lovelace/cards/types";
import type { CallServiceConfig } from "../../../../src/panels/lovelace/entity-rows/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -254,169 +260,194 @@ const ENTITIES = [
},
];
type GalleryEntitiesCardConfig = Omit<EntitiesCardConfig, "entities"> & {
type: EntitiesCardConfig["type"];
entities: (
| EntitiesCardConfig["entities"][number]
| Pick<EntitiesCardEntityConfig, "entity" | "secondary_info">
| Omit<CallServiceConfig, "entity">
)[];
};
const CONFIGS = [
{
heading: "Basic",
config: `
- type: entities
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- light.non_existing
- climate.ecobee
- input_number.number
- sensor.humidity
- text.message
- event.doorbell
`,
config: {
type: "entities",
entities: [
"scene.romantic_lights",
"device_tracker.demo_paulus",
"cover.kitchen_window",
"group.kitchen",
"lock.kitchen_door",
"light.bed_light",
"light.non_existing",
"climate.ecobee",
"input_number.number",
"sensor.humidity",
"text.message",
"event.doorbell",
],
},
},
{
heading: "With enabled state color",
config: `
- type: entities
state_color: true
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- light.non_existing
- climate.ecobee
- input_number.number
- sensor.humidity
- text.message
`,
config: {
type: "entities",
state_color: true,
entities: [
"scene.romantic_lights",
"device_tracker.demo_paulus",
"cover.kitchen_window",
"group.kitchen",
"lock.kitchen_door",
"light.bed_light",
"light.non_existing",
"climate.ecobee",
"input_number.number",
"sensor.humidity",
"text.message",
],
},
},
{
heading: "Helpers",
config: `
- type: entities
title: Helpers
entities:
- entity: input_boolean.toggle
- entity: input_datetime.date_and_time
- entity: input_number.number
- entity: input_select.dropdown
- entity: input_text.text
- entity: timer.timer
- entity: counter.counter
`,
config: {
type: "entities",
title: "Helpers",
entities: [
{ entity: "input_boolean.toggle" },
{ entity: "input_datetime.date_and_time" },
{ entity: "input_number.number" },
{ entity: "input_select.dropdown" },
{ entity: "input_text.text" },
{ entity: "timer.timer" },
{ entity: "counter.counter" },
],
},
},
{
heading: "With title, toggle-able",
config: `
- type: entities
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- climate.ecobee
- input_number.number
title: Random group
`,
config: {
type: "entities",
entities: [
"scene.romantic_lights",
"device_tracker.demo_paulus",
"cover.kitchen_window",
"group.kitchen",
"lock.kitchen_door",
"light.bed_light",
"climate.ecobee",
"input_number.number",
],
title: "Random group",
},
},
{
heading: "With title, toggle = false",
config: `
- type: entities
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- climate.ecobee
- input_number.number
title: Random group
show_header_toggle: false
`,
config: {
type: "entities",
entities: [
"scene.romantic_lights",
"device_tracker.demo_paulus",
"cover.kitchen_window",
"group.kitchen",
"lock.kitchen_door",
"light.bed_light",
"climate.ecobee",
"input_number.number",
],
title: "Random group",
show_header_toggle: false,
},
},
{
heading: "With title, can't toggle",
config: `
- type: entities
entities:
- device_tracker.demo_paulus
title: Random group
`,
config: {
type: "entities",
entities: ["device_tracker.demo_paulus"],
title: "Random group",
},
},
{
heading: "Unavailable",
config: `
- type: entities
entities:
- scene.unavailable
- device_tracker.unavailable
- cover.unavailable
- lock.unavailable
- light.unavailable
- climate.unavailable
- input_number.unavailable
- input_select.unavailable
- text.unavailable
- event.unavailable
`,
config: {
type: "entities",
entities: [
"scene.unavailable",
"device_tracker.unavailable",
"cover.unavailable",
"lock.unavailable",
"light.unavailable",
"climate.unavailable",
"input_number.unavailable",
"input_select.unavailable",
"text.unavailable",
"event.unavailable",
],
},
},
{
heading: "Custom name, secondary info, custom icon",
config: `
- type: entities
entities:
- entity: scene.romantic_lights
name: ¯\\_(ツ)_/¯
- entity: device_tracker.demo_paulus
secondary_info: entity-id
- entity: cover.kitchen_window
secondary_info: last-changed
- entity: group.kitchen
icon: mdi:home-assistant
- lock.kitchen_door
- entity: light.bed_light
icon: mdi:alarm-light
name: Bed Light Custom Icon
- climate.ecobee
- input_number.number
title: Random group
show_header_toggle: false
`,
config: {
type: "entities",
entities: [
{ entity: "scene.romantic_lights", name: "¯\\_(ツ)_/¯" },
{
entity: "device_tracker.demo_paulus",
secondary_info: "entity-id",
},
{
entity: "cover.kitchen_window",
secondary_info: "last-changed",
},
{ entity: "group.kitchen", icon: "mdi:home-assistant" },
"lock.kitchen_door",
{
entity: "light.bed_light",
icon: "mdi:alarm-light",
name: "Bed Light Custom Icon",
},
"climate.ecobee",
"input_number.number",
],
title: "Random group",
show_header_toggle: false,
},
},
{
heading: "Special rows",
config: `
- type: entities
entities:
- type: perform-action
icon: mdi:power
name: Bed light
action_name: Toggle light
action: light.toggle
data:
entity_id: light.bed_light
- type: section
label: Links
- type: weblink
url: http://google.com/
icon: mdi:google
name: Google
- type: divider
- type: divider
style:
height: 30px
margin: 4px 0
background: center / contain url("/images/divider.png") no-repeat
`,
config: {
type: "entities",
entities: [
{
type: "perform-action",
icon: "mdi:power",
name: "Bed light",
action_name: "Toggle light",
action: "light.toggle",
data: { entity_id: "light.bed_light" },
},
{ type: "section", label: "Links" },
{
type: "weblink",
url: "http://google.com/",
icon: "mdi:google",
name: "Google",
},
{ type: "divider" },
{
type: "divider",
style: {
height: "30px",
margin: "4px 0",
background: 'center / contain url("/images/divider.png") no-repeat',
},
},
],
},
},
];
] satisfies DemoCardConfig<GalleryEntitiesCardConfig>[];
@customElement("demo-lovelace-entities-card")
class DemoEntities extends LitElement {
@@ -1,6 +1,8 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { ButtonCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -18,60 +20,64 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Basic example",
config: `
- type: button
entity: light.bed_light
`,
config: {
type: "button",
entity: "light.bed_light",
},
},
{
heading: "With Name (defined in card)",
config: `
- type: button
name: Custom Name
entity: light.bed_light
`,
config: {
type: "button",
name: "Custom Name",
entity: "light.bed_light",
},
},
{
heading: "With Icon",
config: `
- type: button
entity: light.bed_light
icon: mdi:tools
`,
config: {
type: "button",
entity: "light.bed_light",
icon: "mdi:tools",
},
},
{
heading: "With State",
config: `
- type: button
entity: light.bed_light
show_state: true
`,
config: {
type: "button",
entity: "light.bed_light",
show_state: true,
},
},
{
heading: "Custom Tap Action (toggle)",
config: `
- type: button
entity: light.bed_light
tap_action:
action: toggle
`,
config: {
type: "button",
entity: "light.bed_light",
tap_action: {
action: "toggle",
},
},
},
{
heading: "Running Service",
config: `
- type: button
entity: light.bed_light
service: light.toggle
`,
config: {
type: "button",
entity: "light.bed_light",
tap_action: {
action: "perform-action",
perform_action: "light.toggle",
},
},
},
{
heading: "Invalid Entity",
config: `
- type: button
entity: sensor.invalid_entity
`,
config: {
type: "button",
entity: "sensor.invalid_entity",
},
},
];
] satisfies DemoCardConfig<ButtonCardConfig>[];
@customElement("demo-lovelace-entity-button-card")
class DemoButtonEntity extends LitElement {
+160 -141
View File
@@ -1,7 +1,12 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
EntitiesCardConfig,
EntityFilterCardConfig,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -114,184 +119,198 @@ const ENTITIES = [
},
];
const CONFIGS = [
type StateFilterEntityFilterCardConfig = Pick<
EntityFilterCardConfig,
"type" | "entities" | "card" | "show_empty"
> & {
conditions?: never;
state_filter: NonNullable<EntityFilterCardConfig["state_filter"]>;
};
const VALID_CONFIGS = [
{
heading: "Unfiltered entities",
config: `
- type: entities
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
`,
config: {
type: "entities",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
},
},
{
heading: "On and home entities",
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
conditions:
- condition: state
state:
- "on"
- home
`,
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
conditions: [{ condition: "state", state: ["on", "home"] }],
},
},
{
heading: "Same state as Bed Light",
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
conditions:
- condition: state
state:
- light.bed_light
`,
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
conditions: [{ condition: "state", state: ["light.bed_light"] }],
},
},
{
heading: 'With "entities" card config',
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
conditions:
- condition: state
state:
- "on"
- home
card:
type: entities
title: Custom Title
show_header_toggle: false
`,
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
conditions: [{ condition: "state", state: ["on", "home"] }],
card: {
type: "entities",
title: "Custom Title",
show_header_toggle: false,
},
},
},
{
heading: 'With "glance" card config',
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
conditions:
- condition: state
state:
- "on"
- home
card:
type: glance
show_state: true
title: Custom Title
`,
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
conditions: [{ condition: "state", state: ["on", "home"] }],
card: {
type: "glance",
show_state: true,
title: "Custom Title",
},
},
},
{
heading:
"Filtered entities by battery attribute (< '30') using state filter",
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
state_filter:
- operator: <
attribute: battery
value: "30"
`,
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
],
state_filter: [{ operator: "<", attribute: "battery", value: "30" }],
},
},
{
heading: "Unfiltered number entities",
config: `
- type: entities
entities:
- input_number.min_battery_level
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
`,
config: {
type: "entities",
entities: [
"input_number.min_battery_level",
"sensor.battery_1",
"sensor.battery_3",
"sensor.battery_2",
"sensor.battery_4",
],
},
},
{
heading: "Battery lower than 50%",
config: `
- type: entity-filter
entities:
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
conditions:
- condition: numeric_state
below: 50
`,
config: {
type: "entity-filter",
entities: [
"sensor.battery_1",
"sensor.battery_3",
"sensor.battery_2",
"sensor.battery_4",
],
conditions: [{ condition: "numeric_state", below: 50 }],
},
},
{
heading: "Battery lower than min battery level",
config: `
- type: entity-filter
entities:
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
conditions:
- condition: numeric_state
below: input_number.min_battery_level
`,
config: {
type: "entity-filter",
entities: [
"sensor.battery_1",
"sensor.battery_3",
"sensor.battery_2",
"sensor.battery_4",
],
conditions: [
{ condition: "numeric_state", below: "input_number.min_battery_level" },
],
},
},
{
heading: "Battery between min battery level and 70%",
config: `
- type: entity-filter
entities:
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
conditions:
- condition: numeric_state
above: input_number.min_battery_level
below: 70
`,
config: {
type: "entity-filter",
entities: [
"sensor.battery_1",
"sensor.battery_3",
"sensor.battery_2",
"sensor.battery_4",
],
conditions: [
{
condition: "numeric_state",
above: "input_number.min_battery_level",
below: 70,
},
],
},
},
] satisfies DemoCardConfig<
| EntitiesCardConfig
| EntityFilterCardConfig
| StateFilterEntityFilterCardConfig
>[];
const INVALID_CONFIGS = [
{
heading: "Error: Entities must be specified",
config: `
- type: entity-filter
`,
config: { type: "entity-filter" },
expectConfigError: true,
},
{
heading: "Error: Incorrect filter config",
config: `
- type: entity-filter
entities:
- sensor.gas_station_lowest_price
`,
config: {
type: "entity-filter",
entities: ["sensor.gas_station_lowest_price"],
},
expectConfigError: true,
},
];
] satisfies DemoCardConfig<
| Pick<EntityFilterCardConfig, "type">
| Pick<EntityFilterCardConfig, "type" | "entities">
>[];
const CONFIGS = [...VALID_CONFIGS, ...INVALID_CONFIGS];
@customElement("demo-lovelace-entity-filter-card")
class DemoEntityFilter extends LitElement {
+115 -115
View File
@@ -1,7 +1,9 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { GaugeCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -30,158 +32,156 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Basic example",
config: `
- type: gauge
entity: sensor.outside_humidity
name: Outside Humidity
`,
config: {
type: "gauge",
entity: "sensor.outside_humidity",
name: "Outside Humidity",
},
},
{
heading: "Custom unit of measurement",
config: `
- type: gauge
entity: sensor.outside_temperature
unit_of_measurement: C
name: Outside Temperature
`,
config: {
type: "gauge",
entity: "sensor.outside_temperature",
unit: "C",
name: "Outside Temperature",
},
},
{
heading: "Rendering needle",
config: `
- type: gauge
entity: sensor.outside_humidity
name: Outside Humidity
needle: true
`,
config: {
type: "gauge",
entity: "sensor.outside_humidity",
name: "Outside Humidity",
needle: true,
},
},
{
heading: "Rendering needle and severity levels",
config: `
- type: gauge
entity: sensor.brightness_high
name: Brightness High
needle: true
severity:
red: 75
green: 0
yellow: 50
`,
config: {
type: "gauge",
entity: "sensor.brightness_high",
name: "Brightness High",
needle: true,
severity: {
red: 75,
green: 0,
yellow: 50,
},
},
},
{
heading: "Setting severity levels",
config: `
- type: gauge
entity: sensor.brightness
name: Brightness Low
severity:
red: 75
green: 0
yellow: 50
`,
config: {
type: "gauge",
entity: "sensor.brightness",
name: "Brightness Low",
severity: {
red: 75,
green: 0,
yellow: 50,
},
},
},
{
heading: "Setting severity levels",
config: `
- type: gauge
entity: sensor.brightness_medium
name: Brightness Medium
severity:
red: 75
green: 0
yellow: 50
`,
config: {
type: "gauge",
entity: "sensor.brightness_medium",
name: "Brightness Medium",
severity: {
red: 75,
green: 0,
yellow: 50,
},
},
},
{
heading: "Setting severity levels",
config: `
- type: gauge
entity: sensor.brightness_high
name: Brightness High
severity:
red: 75
green: 0
yellow: 50
`,
config: {
type: "gauge",
entity: "sensor.brightness_high",
name: "Brightness High",
severity: {
red: 75,
green: 0,
yellow: 50,
},
},
},
{
heading: "Setting min (0) and mx (15) values",
config: `
- type: gauge
entity: sensor.brightness
name: Brightness
min: 0
max: 15
`,
config: {
type: "gauge",
entity: "sensor.brightness",
name: "Brightness",
min: 0,
max: 15,
},
},
{
heading: "Invalid entity",
config: `
- type: gauge
entity: sensor.invalid_entity
`,
config: {
type: "gauge",
entity: "sensor.invalid_entity",
},
},
{
heading: "Non-numeric value",
config: `
- type: gauge
entity: plant.bonsai
`,
config: {
type: "gauge",
entity: "plant.bonsai",
},
},
{
heading: "Unavailable entity",
config: `
- type: gauge
entity: sensor.not_working
`,
config: {
type: "gauge",
entity: "sensor.not_working",
},
},
{
heading: "Lower minimum",
config: `
- type: gauge
entity: sensor.brightness_high
needle: true
severity:
green: 0
yellow: 0.45
red: 0.9
min: -0.05
name: " "
max: 1.9
unit: GBP/h`,
config: {
type: "gauge",
entity: "sensor.brightness_high",
needle: true,
severity: {
green: 0,
yellow: 0.45,
red: 0.9,
},
min: -0.05,
name: " ",
max: 1.9,
unit: "GBP/h",
},
},
{
heading: "A lot of segments",
config: `
- type: gauge
needle: true
name: Percent gauge
entity: sensor.brightness_high
unit: "%"
min: 0
max: 100
segments:
- from: 0
color: "#db4437"
- from: 10
color: "#cc4d39"
- from: 20
color: "#bd563a"
- from: 30
color: "#ad603c"
- from: 40
color: "#9e693d"
- from: 50
color: "#8f723f"
- from: 60
color: "#807b41"
- from: 70
color: "#718442"
- from: 80
color: "#618e44"
- from: 90
color: "#43a047"`,
config: {
type: "gauge",
needle: true,
name: "Percent gauge",
entity: "sensor.brightness_high",
unit: "%",
min: 0,
max: 100,
segments: [
{ from: 0, color: "#db4437" },
{ from: 10, color: "#cc4d39" },
{ from: 20, color: "#bd563a" },
{ from: 30, color: "#ad603c" },
{ from: 40, color: "#9e693d" },
{ from: 50, color: "#8f723f" },
{ from: 60, color: "#807b41" },
{ from: 70, color: "#718442" },
{ from: 80, color: "#618e44" },
{ from: 90, color: "#43a047" },
],
},
},
];
] satisfies DemoCardConfig<GaugeCardConfig>[];
@customElement("demo-lovelace-gauge-card")
class DemoGaugeEntity extends LitElement {
+161 -135
View File
@@ -1,7 +1,12 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
GlanceCardConfig,
GlanceConfigEntity,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -86,172 +91,193 @@ const ENTITIES = [
},
];
type LegacyNullNameGlanceCardConfig = Omit<GlanceCardConfig, "entities"> & {
type: GlanceCardConfig["type"];
entities: (
| string
| GlanceConfigEntity
| (Omit<GlanceConfigEntity, "name"> & { name: null })
)[];
};
const CONFIGS = [
{
heading: "Basic example",
config: `
- type: glance
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
config: {
type: "glance",
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
},
{
heading: "No state colors",
config: `
- type: glance
state_color: false
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
config: {
type: "glance",
state_color: false,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
},
{
heading: "With title",
config: `
- type: glance
title: Custom title
columns: 4
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
config: {
type: "glance",
title: "Custom title",
columns: 4,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
},
{
heading: "Custom number of columns",
config: `
- type: glance
columns: 7
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
config: {
type: "glance",
columns: 7,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
},
{
heading: "No entity names",
config: `
- type: glance
columns: 4
show_name: false
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
config: {
type: "glance",
columns: 4,
show_name: false,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
},
{
heading: "No state labels",
config: `
- type: glance
columns: 4
show_state: false
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
config: {
type: "glance",
columns: 4,
show_state: false,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
},
{
heading: "No names and no state labels",
config: `
- type: glance
columns: 4
show_name: false
show_state: false
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
config: {
type: "glance",
columns: 4,
show_name: false,
show_state: false,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
},
{
heading: "Custom name + custom icon",
config: `
- type: glance
columns: 4
entities:
- entity: device_tracker.demo_paulus
name: ¯\\_(ツ)_/¯
icon: mdi:home-assistant
- entity: media_player.living_room
name: ¯\\_(ツ)_/¯
icon: mdi:home-assistant
`,
config: {
type: "glance",
columns: 4,
entities: [
{
entity: "device_tracker.demo_paulus",
name: "¯\\_(ツ)_/¯",
icon: "mdi:home-assistant",
},
{
entity: "media_player.living_room",
name: "¯\\_(ツ)_/¯",
icon: "mdi:home-assistant",
},
],
},
},
{
heading: "Selectively hidden name",
config: `
- type: glance
columns: 4
entities:
- device_tracker.demo_paulus
- entity: media_player.living_room
name:
- sun.sun
- entity: cover.kitchen_window
name:
- light.kitchen_lights
- entity: lock.kitchen_door
name:
- light.ceiling_lights
`,
config: {
type: "glance",
columns: 4,
entities: [
"device_tracker.demo_paulus",
{ entity: "media_player.living_room", name: null },
"sun.sun",
{ entity: "cover.kitchen_window", name: null },
"light.kitchen_lights",
{ entity: "lock.kitchen_door", name: null },
"light.ceiling_lights",
],
},
},
{
heading: "Custom tap action",
config: `
- type: glance
columns: 4
entities:
- entity: lock.kitchen_door
name: Custom
tap_action:
type: toggle
- entity: light.ceiling_lights
name: Custom
tap_action:
action: call-service
service: light.turn_on
data:
entity_id: light.ceiling_lights
- entity: sun.sun
name: Regular
- entity: light.kitchen_lights
name: Regular
`,
config: {
type: "glance",
columns: 4,
entities: [
{
entity: "lock.kitchen_door",
name: "Custom",
tap_action: { action: "toggle" },
},
{
entity: "light.ceiling_lights",
name: "Custom",
tap_action: {
action: "perform-action",
perform_action: "light.turn_on",
data: { entity_id: "light.ceiling_lights" },
},
},
{ entity: "sun.sun", name: "Regular" },
{ entity: "light.kitchen_lights", name: "Regular" },
],
},
},
];
] satisfies DemoCardConfig<GlanceCardConfig | LegacyNullNameGlanceCardConfig>[];
@customElement("demo-lovelace-glance-card")
class DemoGlanceEntity extends LitElement {
+129 -124
View File
@@ -1,8 +1,13 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { mockHistory } from "../../../../demo/src/stubs/history";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
GridCardConfig,
StackCardConfig,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -70,159 +75,159 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Default Grid",
config: `
- type: grid
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
- type: entity
entity: device_tracker.demo_anne_therese
`,
config: {
type: "grid",
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
{ type: "entity", entity: "device_tracker.demo_paulus" },
{ type: "sensor", entity: "sensor.illumination", graph: "line" },
{ type: "entity", entity: "device_tracker.demo_anne_therese" },
],
},
},
{
heading: "Non-square Grid with 2 columns",
config: `
- type: grid
columns: 2
square: false
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
`,
config: {
type: "grid",
columns: 2,
square: false,
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
{ type: "entity", entity: "device_tracker.demo_paulus" },
{ type: "sensor", entity: "sensor.illumination", graph: "line" },
],
},
},
{
heading: "Default Grid with title",
config: `
- type: grid
title: Kitchen
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
- type: entity
entity: device_tracker.demo_anne_therese
`,
config: {
type: "grid",
title: "Kitchen",
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
{ type: "entity", entity: "device_tracker.demo_paulus" },
{ type: "sensor", entity: "sensor.illumination", graph: "line" },
{ type: "entity", entity: "device_tracker.demo_anne_therese" },
],
},
},
{
heading: "Columns 4",
config: `
- type: grid
columns: 4
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
`,
config: {
type: "grid",
columns: 4,
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
{ type: "entity", entity: "device_tracker.demo_paulus" },
{ type: "sensor", entity: "sensor.illumination", graph: "line" },
],
},
},
{
heading: "Columns 2",
config: `
- type: grid
columns: 2
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
`,
config: {
type: "grid",
columns: 2,
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
],
},
},
{
heading: "Columns 1",
config: `
- type: grid
columns: 1
cards:
- type: entity
entity: light.kitchen_lights
`,
config: {
type: "grid",
columns: 1,
cards: [{ type: "entity", entity: "light.kitchen_lights" }],
},
},
{
heading: "Size for single card",
config: `
- type: grid
cards:
- type: entity
entity: light.kitchen_lights
`,
config: {
type: "grid",
cards: [{ type: "entity", entity: "light.kitchen_lights" }],
},
},
{
heading: "Vertical Stack",
config: `
- type: vertical-stack
cards:
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
- type: glance
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
`,
config: {
type: "vertical-stack",
cards: [
{
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
},
{
type: "glance",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
],
},
],
},
},
{
heading: "Horizontal Stack",
config: `
- type: horizontal-stack
cards:
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
- type: glance
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
`,
config: {
type: "horizontal-stack",
cards: [
{
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
},
{
type: "glance",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
],
},
],
},
},
{
heading: "Combination of both",
config: `
- type: vertical-stack
cards:
- type: horizontal-stack
cards:
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
- type: glance
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- type: picture-entity
image: /images/bed.png
entity: light.bed_light
`,
config: {
type: "vertical-stack",
cards: [
{
type: "horizontal-stack",
cards: [
{
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
},
{
type: "glance",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
],
},
],
},
{
type: "picture-entity",
image: "/images/bed.png",
entity: "light.bed_light",
},
],
},
},
];
] satisfies DemoCardConfig<GridCardConfig | StackCardConfig>[];
@customElement("demo-lovelace-grid-and-stack-card")
class DemoStack extends LitElement {
+22 -20
View File
@@ -1,42 +1,44 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { IframeCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
const CONFIGS = [
{
heading: "Without title",
config: `
- type: iframe
url: https://embed.windy.com/embed2.html
`,
config: {
type: "iframe",
url: "https://embed.windy.com/embed2.html",
},
},
{
heading: "With title",
config: `
- type: iframe
url: https://embed.windy.com/embed2.html
title: Weather radar
`,
config: {
type: "iframe",
url: "https://embed.windy.com/embed2.html",
title: "Weather radar",
},
},
{
heading: "Height-Width 3:4",
config: `
- type: iframe
url: https://embed.windy.com/embed2.html
aspect_ratio: 75%
`,
config: {
type: "iframe",
url: "https://embed.windy.com/embed2.html",
aspect_ratio: "75%",
},
},
{
heading: "Height-Width 1:1",
config: `
- type: iframe
url: https://embed.windy.com/embed2.html
aspect_ratio: 100%
`,
config: {
type: "iframe",
url: "https://embed.windy.com/embed2.html",
aspect_ratio: "100%",
},
},
];
] satisfies DemoCardConfig<IframeCardConfig>[];
@customElement("demo-lovelace-iframe-card")
class DemoIframe extends LitElement {
+23 -21
View File
@@ -1,6 +1,8 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { LightCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -44,40 +46,40 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Switchable Light",
config: `
- type: light
entity: light.bed_light
`,
config: {
type: "light",
entity: "light.bed_light",
},
},
{
heading: "Dimmable Light On",
config: `
- type: light
entity: light.dim_on
`,
config: {
type: "light",
entity: "light.dim_on",
},
},
{
heading: "Dimmable Light Off",
config: `
- type: light
entity: light.dim_off
`,
config: {
type: "light",
entity: "light.dim_off",
},
},
{
heading: "Unavailable",
config: `
- type: light
entity: light.unavailable
`,
config: {
type: "light",
entity: "light.unavailable",
},
},
{
heading: "Non existing",
config: `
- type: light
entity: light.nonexisting
`,
config: {
type: "light",
entity: "light.nonexisting",
},
},
];
] satisfies DemoCardConfig<LightCardConfig>[];
@customElement("demo-lovelace-light-card")
class DemoLightEntity extends LitElement {
+58 -70
View File
@@ -1,7 +1,9 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { MapCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
const ENTITIES = [
@@ -86,107 +88,93 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Without title",
config: `
- type: map
entities:
- entity: device_tracker.demo_paulus
- device_tracker.demo_home_boy
- zone.home
`,
config: {
type: "map",
entities: [
{ entity: "device_tracker.demo_paulus" },
"device_tracker.demo_home_boy",
"zone.home",
],
},
},
{
heading: "With title",
config: `
- type: map
entities:
- entity: device_tracker.demo_paulus
- zone.home
title: Where is Paulus?
`,
config: {
type: "map",
entities: [{ entity: "device_tracker.demo_paulus" }, "zone.home"],
title: "Where is Paulus?",
},
},
{
heading: "Height-Width 1:2",
config: `
- type: map
entities:
- entity: device_tracker.demo_paulus
- zone.home
aspect_ratio: 50%
`,
config: {
type: "map",
entities: [{ entity: "device_tracker.demo_paulus" }, "zone.home"],
aspect_ratio: "50%",
},
},
{
heading: "Default Zoom",
config: `
- type: map
default_zoom: 12
entities:
- entity: device_tracker.demo_paulus
- zone.home
`,
config: {
type: "map",
default_zoom: 12,
entities: [{ entity: "device_tracker.demo_paulus" }, "zone.home"],
},
},
{
heading: "Default Zoom too High",
config: `
- type: map
default_zoom: 20
entities:
- entity: device_tracker.demo_paulus
- zone.home
`,
config: {
type: "map",
default_zoom: 20,
entities: [{ entity: "device_tracker.demo_paulus" }, "zone.home"],
},
},
{
heading: "Single Marker",
config: `
- type: map
entities:
- device_tracker.demo_paulus
`,
config: {
type: "map",
entities: ["device_tracker.demo_paulus"],
},
},
{
heading: "Single Marker Default Zoom",
config: `
- type: map
default_zoom: 8
entities:
- device_tracker.demo_paulus
`,
config: {
type: "map",
default_zoom: 8,
entities: ["device_tracker.demo_paulus"],
},
},
{
heading: "No Entities",
config: `
- type: map
entities:
- light.bed_light
`,
config: {
type: "map",
entities: ["light.bed_light"],
},
},
{
heading: "No Entities, Default Zoom",
config: `
- type: map
default_zoom: 8
entities:
- light.bed_light
`,
config: {
type: "map",
default_zoom: 8,
entities: ["light.bed_light"],
},
},
{
heading: "Geo Location Entities",
config: `
- type: map
geo_location_sources:
- bushfire_demo
`,
config: {
type: "map",
geo_location_sources: ["bushfire_demo"],
},
},
{
heading: "Geo Location Entities with Home Zone",
config: `
- type: map
geo_location_sources:
- bushfire_demo
entities:
- zone.bushfire
`,
config: {
type: "map",
geo_location_sources: ["bushfire_demo"],
entities: ["zone.bushfire"],
},
},
];
] satisfies DemoCardConfig<MapCardConfig>[];
@customElement("demo-lovelace-map-card")
class DemoMap extends LitElement {
+10 -6
View File
@@ -1,17 +1,18 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { mockTemplate } from "../../../../demo/src/stubs/template";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { MarkdownCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
const CONFIGS = [
{
heading: "markdown-it demo",
config: `
- type: markdown
content: |
# h1 Heading 8-)
config: {
type: "markdown",
content: `# h1 Heading 8-)
## h2 Heading
@@ -278,9 +279,12 @@ const CONFIGS = [
<ha-alert alert-type="success">This is a success alert — check it out!</ha-alert>
<ha-alert title="Test alert">This is an alert with a title</ha-alert>
`,
`
.replace(/^ {4}/gm, "")
.replace(/\n+$/, "\n"),
},
},
];
] satisfies DemoCardConfig<MarkdownCardConfig>[];
@customElement("demo-lovelace-markdown-card")
class DemoMarkdown extends LitElement {
@@ -1,162 +1,165 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
GridCardConfig,
MediaControlCardConfig,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { createMediaPlayerEntities } from "../../data/media_players";
const CONFIGS = [
{
heading: "Paused Music",
config: `
- type: media-control
entity: media_player.music_paused
`,
config: {
type: "media-control",
entity: "media_player.music_paused",
},
},
{
heading: "Playing Music",
config: `
- type: media-control
entity: media_player.music_playing
`,
config: {
type: "media-control",
entity: "media_player.music_playing",
},
},
{
heading: "Playing Stream",
config: `
- type: media-control
entity: media_player.stream_playing
`,
config: {
type: "media-control",
entity: "media_player.stream_playing",
},
},
{
heading: "Paused Stream",
config: `
- type: media-control
entity: media_player.stream_paused
`,
config: {
type: "media-control",
entity: "media_player.stream_paused",
},
},
{
heading: 'Playing Stream (with "previous" support)',
config: `
- type: media-control
entity: media_player.stream_playing_previous
`,
config: {
type: "media-control",
entity: "media_player.stream_playing_previous",
},
},
{
heading: "Playing non-skip TV Show",
config: `
- type: media-control
entity: media_player.tv_playing
`,
config: {
type: "media-control",
entity: "media_player.tv_playing",
},
},
{
heading: "Screen Casting",
config: `
- type: media-control
entity: media_player.android_cast
`,
config: {
type: "media-control",
entity: "media_player.android_cast",
},
},
{
heading: "Digital Picture Frame",
config: `
- type: media-control
entity: media_player.image_display
`,
config: {
type: "media-control",
entity: "media_player.image_display",
},
},
{
heading: "Sonos Idle",
config: `
- type: media-control
entity: media_player.sonos_idle
`,
config: {
type: "media-control",
entity: "media_player.sonos_idle",
},
},
{
heading: "Idle waiting for Browse Media",
config: `
- type: media-control
entity: media_player.idle_browse_media
`,
config: {
type: "media-control",
entity: "media_player.idle_browse_media",
},
},
{
heading: "Player Off",
config: `
- type: media-control
entity: media_player.theater_off
`,
config: {
type: "media-control",
entity: "media_player.theater_off",
},
},
{
heading: "Player On",
config: `
- type: media-control
entity: media_player.theater_on
`,
config: {
type: "media-control",
entity: "media_player.theater_on",
},
},
{
heading: "Player Off (cannot be switched on)",
config: `
- type: media-control
entity: media_player.theater_off_static
`,
config: {
type: "media-control",
entity: "media_player.theater_off_static",
},
},
{
heading: "Player On (cannot be switched off)",
config: `
- type: media-control
entity: media_player.theater_on_static
`,
config: {
type: "media-control",
entity: "media_player.theater_on_static",
},
},
{
heading: "Player Idle",
config: `
- type: media-control
entity: media_player.idle
`,
config: {
type: "media-control",
entity: "media_player.idle",
},
},
{
heading: "Player Playing",
config: `
- type: media-control
entity: media_player.playing
`,
config: {
type: "media-control",
entity: "media_player.playing",
},
},
{
heading: "Player Unavailable",
config: `
- type: media-control
entity: media_player.unavailable
`,
config: {
type: "media-control",
entity: "media_player.unavailable",
},
},
{
heading: "Player Unknown",
config: `
- type: media-control
entity: media_player.unknown
`,
config: {
type: "media-control",
entity: "media_player.unknown",
},
},
{
heading: "Receiver On (selectable sources)",
config: `
- type: media-control
entity: media_player.receiver_on
`,
config: {
type: "media-control",
entity: "media_player.receiver_on",
},
},
{
heading: "Receiver Off (selectable sources)",
config: `
- type: media-control
entity: media_player.receiver_off
`,
config: {
type: "media-control",
entity: "media_player.receiver_off",
},
},
{
heading: "Grid Full Size",
config: `
- type: grid
columns: 1
cards:
- type: media-control
entity: media_player.music_paused
`,
config: {
type: "grid",
columns: 1,
cards: [{ type: "media-control", entity: "media_player.music_paused" }],
},
},
];
] satisfies DemoCardConfig<MediaControlCardConfig | GridCardConfig>[];
@customElement("demo-lovelace-media-control-card")
class DemoHuiMediaControlCard extends LitElement {
+46 -45
View File
@@ -1,59 +1,60 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { EntitiesCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { createMediaPlayerEntities } from "../../data/media_players";
const CONFIGS = [
{
heading: "Media Players",
config: `
- type: entities
entities:
- entity: media_player.music_paused
name: Paused Music
- entity: media_player.music_playing
name: Playing Music
- entity: media_player.stream_playing
name: Playing Stream
- entity: media_player.stream_paused
name: Paused Stream
- entity: media_player.stream_playing_previous
name: Playing Stream (with "previous" support)
- entity: media_player.tv_playing
name: Playing non-skip TV Show
- entity: media_player.android_cast
name: Screen casting
- entity: media_player.image_display
name: Digital Picture Frame
- entity: media_player.sonos_idle
name: Sonos Idle
- entity: media_player.idle_browse_media
name: Idle waiting for Browse Media
- entity: media_player.theater_off
name: Player Off
- entity: media_player.theater_on
name: Player On
- entity: media_player.theater_off_static
name: Player Off (cannot be switched on)
- entity: media_player.theater_on_static
name: Player On (cannot be switched off)
- entity: media_player.idle
name: Player Idle
- entity: media_player.playing
name: Player Playing
- entity: media_player.unavailable
name: Player Unavailable
- entity: media_player.unknown
name: Player Unknown
- entity: media_player.receiver_on
name: Receiver On (selectable sources)
- entity: media_player.receiver_off
name: Receiver Off (selectable sources)
`,
config: {
type: "entities",
entities: [
{ entity: "media_player.music_paused", name: "Paused Music" },
{ entity: "media_player.music_playing", name: "Playing Music" },
{ entity: "media_player.stream_playing", name: "Playing Stream" },
{ entity: "media_player.stream_paused", name: "Paused Stream" },
{
entity: "media_player.stream_playing_previous",
name: 'Playing Stream (with "previous" support)',
},
{ entity: "media_player.tv_playing", name: "Playing non-skip TV Show" },
{ entity: "media_player.android_cast", name: "Screen casting" },
{ entity: "media_player.image_display", name: "Digital Picture Frame" },
{ entity: "media_player.sonos_idle", name: "Sonos Idle" },
{
entity: "media_player.idle_browse_media",
name: "Idle waiting for Browse Media",
},
{ entity: "media_player.theater_off", name: "Player Off" },
{ entity: "media_player.theater_on", name: "Player On" },
{
entity: "media_player.theater_off_static",
name: "Player Off (cannot be switched on)",
},
{
entity: "media_player.theater_on_static",
name: "Player On (cannot be switched off)",
},
{ entity: "media_player.idle", name: "Player Idle" },
{ entity: "media_player.playing", name: "Player Playing" },
{ entity: "media_player.unavailable", name: "Player Unavailable" },
{ entity: "media_player.unknown", name: "Player Unknown" },
{
entity: "media_player.receiver_on",
name: "Receiver On (selectable sources)",
},
{
entity: "media_player.receiver_off",
name: "Receiver Off (selectable sources)",
},
],
},
},
];
] satisfies DemoCardConfig<EntitiesCardConfig>[];
@customElement("demo-lovelace-media-player-row")
export class DemoLovelaceMediaPlayerRow extends LitElement {
+15 -13
View File
@@ -1,6 +1,8 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { PictureCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -19,26 +21,26 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Image URL",
config: `
- type: picture
image: /images/living_room.png
`,
config: {
type: "picture",
image: "/images/living_room.png",
},
},
{
heading: "Person entity",
config: `
- type: picture
image_entity: person.paulus
`,
config: {
type: "picture",
image_entity: "person.paulus",
},
},
{
heading: "Error: Image required",
config: `
- type: picture
entity: person.paulus
`,
config: {
type: "picture",
},
expectConfigError: true,
},
];
] satisfies DemoCardConfig<PictureCardConfig>[];
@customElement("demo-lovelace-picture-card")
class DemoPicture extends LitElement {
@@ -1,7 +1,13 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { PictureElementsCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type {
ImageElementConfig,
LovelaceElementConfig,
} from "../../../../src/panels/lovelace/elements/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -60,116 +66,141 @@ const ENTITIES = [
},
];
type LegacyImageElementConfig = Omit<
ImageElementConfig,
"state_filter" | "state_image"
> & {
state_filter?: Record<string, string>;
state_image?: Record<string, string>;
};
type GalleryPictureElementsCardConfig = Omit<
PictureElementsCardConfig,
"elements"
> & {
type: PictureElementsCardConfig["type"];
elements: (LovelaceElementConfig | LegacyImageElementConfig)[];
};
const CONFIGS = [
{
heading: "Card with few elements",
config: `
- type: picture-elements
image: /images/floorplan.png
elements:
- type: service-button
title: Lights Off
style:
top: 97%
left: 90%
padding: 0px
service: light.turn_off
data:
entity_id: group.all_lights
- type: icon
icon: mdi:cctv
entity: camera.demo_camera
style:
top: 12%
left: 6%
transform: rotate(-60deg) scaleX(-1)
--mdc-icon-size: 30px
--mdc-icon-stroke-color: black
--mdc-icon-fill-color: rgba(50, 50, 50, .75)
- type: image
entity: light.bed_light
tap_action:
action: toggle
image: /images/light_bulb_off.png
state_image:
'on': /images/light_bulb_on.png
state_filter:
'on': brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)
'off': brightness(80%) saturate(0.8)
style:
top: 35%
left: 65%
width: 7%
padding: 50px 50px 100px 50px
- type: state-icon
entity: binary_sensor.movement_backyard
style:
top: 8%
left: 35%
`,
config: {
type: "picture-elements",
image: "/images/floorplan.png",
elements: [
{
type: "service-button",
title: "Lights Off",
style: { top: "97%", left: "90%", padding: "0px" },
service: "light.turn_off",
data: { entity_id: "group.all_lights" },
},
{
type: "icon",
icon: "mdi:cctv",
entity: "camera.demo_camera",
style: {
top: "12%",
left: "6%",
transform: "rotate(-60deg) scaleX(-1)",
"--mdc-icon-size": "30px",
"--mdc-icon-stroke-color": "black",
"--mdc-icon-fill-color": "rgba(50, 50, 50, .75)",
},
},
{
type: "image",
entity: "light.bed_light",
tap_action: { action: "toggle" },
image: "/images/light_bulb_off.png",
state_image: { on: "/images/light_bulb_on.png" },
state_filter: {
on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)",
},
style: {
top: "35%",
left: "65%",
width: "7%",
padding: "50px 50px 100px 50px",
},
},
{
type: "state-icon",
entity: "binary_sensor.movement_backyard",
style: { top: "8%", left: "35%" },
},
],
},
},
{
heading: "Card with header",
config: `
- type: picture-elements
image: /images/floorplan.png
title: My House
elements:
- type: service-button
title: Lights Off
style:
top: 97%
left: 90%
padding: 0px
service: light.turn_off
data:
entity_id: group.all_lights
- type: icon
icon: mdi:cctv
entity: camera.demo_camera
style:
top: 12%
left: 6%
transform: rotate(-60deg) scaleX(-1)
--mdc-icon-size: 30px
--mdc-icon-stroke-color: black
--mdc-icon-fill-color: rgba(50, 50, 50, .75)
- type: image
entity: light.bed_light
tap_action:
action: toggle
image: /images/light_bulb_off.png
state_image:
'on': /images/light_bulb_on.png
state_filter:
'on': brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)
'off': brightness(80%) saturate(0.8)
style:
top: 35%
left: 65%
width: 7%
padding: 50px 50px 100px 50px
- type: state-icon
entity: binary_sensor.movement_backyard
style:
top: 8%
left: 35%
`,
config: {
type: "picture-elements",
image: "/images/floorplan.png",
title: "My House",
elements: [
{
type: "service-button",
title: "Lights Off",
style: { top: "97%", left: "90%", padding: "0px" },
service: "light.turn_off",
data: { entity_id: "group.all_lights" },
},
{
type: "icon",
icon: "mdi:cctv",
entity: "camera.demo_camera",
style: {
top: "12%",
left: "6%",
transform: "rotate(-60deg) scaleX(-1)",
"--mdc-icon-size": "30px",
"--mdc-icon-stroke-color": "black",
"--mdc-icon-fill-color": "rgba(50, 50, 50, .75)",
},
},
{
type: "image",
entity: "light.bed_light",
tap_action: { action: "toggle" },
image: "/images/light_bulb_off.png",
state_image: { on: "/images/light_bulb_on.png" },
state_filter: {
on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)",
},
style: {
top: "35%",
left: "65%",
width: "7%",
padding: "50px 50px 100px 50px",
},
},
{
type: "state-icon",
entity: "binary_sensor.movement_backyard",
style: { top: "8%", left: "35%" },
},
],
},
},
{
heading: "Person entity",
config: `
- type: picture-elements
image_entity: person.paulus
elements:
- type: state-icon
entity: sensor.battery
style:
top: 8%
left: 8%
`,
config: {
type: "picture-elements",
image_entity: "person.paulus",
elements: [
{
type: "state-icon",
entity: "sensor.battery",
style: { top: "8%", left: "8%" },
},
],
},
},
];
] satisfies DemoCardConfig<GalleryPictureElementsCardConfig>[];
@customElement("demo-lovelace-picture-elements-card")
class DemoPictureElements extends LitElement {
@@ -1,7 +1,9 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { PictureEntityCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -33,75 +35,73 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "State on",
config: `
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
tap_action:
action: toggle
`,
config: {
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
tap_action: { action: "toggle" },
},
},
{
heading: "State off",
config: `
- type: picture-entity
image: /images/bed.png
entity: light.bed_light
tap_action:
action: toggle
`,
config: {
type: "picture-entity",
image: "/images/bed.png",
entity: "light.bed_light",
tap_action: { action: "toggle" },
},
},
{
heading: "Entity unavailable",
config: `
- type: picture-entity
image: /images/living_room.png
entity: light.non_existing
`,
config: {
type: "picture-entity",
image: "/images/living_room.png",
entity: "light.non_existing",
},
},
{
heading: "Camera entity",
config: `
- type: picture-entity
entity: camera.demo_camera
`,
config: {
type: "picture-entity",
entity: "camera.demo_camera",
},
},
{
heading: "Person entity",
config: `
- type: picture-entity
entity: person.paulus
`,
config: {
type: "picture-entity",
entity: "person.paulus",
},
},
{
heading: "Hidden name",
config: `
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
show_name: false
`,
config: {
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
show_name: false,
},
},
{
heading: "Hidden state",
config: `
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
show_state: false
`,
config: {
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
show_state: false,
},
},
{
heading: "Both hidden",
config: `
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
show_name: false
show_state: false
`,
config: {
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
show_name: false,
show_state: false,
},
},
];
] satisfies DemoCardConfig<PictureEntityCardConfig>[];
@customElement("demo-lovelace-picture-entity-card")
class DemoPictureEntity extends LitElement {
@@ -1,7 +1,9 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { PictureGlanceCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -58,110 +60,110 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Title, dialog, toggle",
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entities:
- switch.decorative_lights
- light.ceiling_lights
- binary_sensor.movement_backyard
- binary_sensor.basement_floor_wet
`,
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entities: [
"switch.decorative_lights",
"light.ceiling_lights",
"binary_sensor.movement_backyard",
"binary_sensor.basement_floor_wet",
],
},
},
{
heading: "Title, dialog, no toggle",
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entities:
- binary_sensor.movement_backyard
- binary_sensor.basement_floor_wet
`,
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entities: [
"binary_sensor.movement_backyard",
"binary_sensor.basement_floor_wet",
],
},
},
{
heading: "Title, no dialog, toggle",
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entities:
- switch.decorative_lights
- light.ceiling_lights
`,
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entities: ["switch.decorative_lights", "light.ceiling_lights"],
},
},
{
heading: "No title, dialog, toggle",
config: `
- type: picture-glance
image: /images/living_room.png
entities:
- switch.decorative_lights
- light.ceiling_lights
- binary_sensor.movement_backyard
- binary_sensor.basement_floor_wet
`,
config: {
type: "picture-glance",
image: "/images/living_room.png",
entities: [
"switch.decorative_lights",
"light.ceiling_lights",
"binary_sensor.movement_backyard",
"binary_sensor.basement_floor_wet",
],
},
},
{
heading: "No title, dialog, no toggle",
config: `
- type: picture-glance
image: /images/living_room.png
entities:
- binary_sensor.movement_backyard
- binary_sensor.basement_floor_wet
`,
config: {
type: "picture-glance",
image: "/images/living_room.png",
entities: [
"binary_sensor.movement_backyard",
"binary_sensor.basement_floor_wet",
],
},
},
{
heading: "No title, no dialog, toggle",
config: `
- type: picture-glance
image: /images/living_room.png
entities:
- switch.decorative_lights
- light.ceiling_lights
`,
config: {
type: "picture-glance",
image: "/images/living_room.png",
entities: ["switch.decorative_lights", "light.ceiling_lights"],
},
},
{
heading: "Person entity",
config: `
- type: picture-glance
image_entity: person.paulus
entities:
- sensor.battery
`,
config: {
type: "picture-glance",
image_entity: "person.paulus",
entities: ["sensor.battery"],
},
},
{
heading: "Custom icon",
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entities:
- entity: switch.decorative_lights
icon: mdi:power
- binary_sensor.basement_floor_wet
`,
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entities: [
{ entity: "switch.decorative_lights", icon: "mdi:power" },
"binary_sensor.basement_floor_wet",
],
},
},
{
heading: "Custom tap action",
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entity: light.ceiling_lights
tap_action:
action: toggle
entities:
- entity: switch.decorative_lights
icon: mdi:power
tap_action:
action: toggle
- binary_sensor.basement_floor_wet
`,
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entity: "light.ceiling_lights",
tap_action: { action: "toggle" },
entities: [
{
entity: "switch.decorative_lights",
icon: "mdi:power",
tap_action: { action: "toggle" },
},
"binary_sensor.basement_floor_wet",
],
},
},
];
] satisfies DemoCardConfig<PictureGlanceCardConfig>[];
@customElement("demo-lovelace-picture-glance-card")
class DemoPictureGlance extends LitElement {
+16 -14
View File
@@ -1,6 +1,8 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { PlantStatusCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { createPlantEntities } from "../../data/plants";
@@ -9,27 +11,27 @@ import { mockIcons } from "../../../../demo/src/stubs/icons";
const CONFIGS = [
{
heading: "Basic example",
config: `
- type: plant-status
entity: plant.lemon_tree
`,
config: {
type: "plant-status",
entity: "plant.lemon_tree",
},
},
{
heading: "Problem (too bright) + low battery",
config: `
- type: plant-status
entity: plant.apple_tree
`,
config: {
type: "plant-status",
entity: "plant.apple_tree",
},
},
{
heading: "With picture + multiple problems",
config: `
- type: plant-status
entity: plant.sunflowers
name: Sunflowers Name Overwrite
`,
config: {
type: "plant-status",
entity: "plant.sunflowers",
name: "Sunflowers Name Overwrite",
},
},
];
] satisfies DemoCardConfig<PlantStatusCardConfig>[];
@customElement("demo-lovelace-plant-card")
export class DemoPlantEntity extends LitElement {
+108 -95
View File
@@ -1,7 +1,9 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { ThermostatCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -123,120 +125,131 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Range example",
config: `
- type: thermostat
entity: climate.ecobee
`,
config: {
type: "thermostat",
entity: "climate.ecobee",
},
},
{
heading: "Single temp example",
config: `
- type: thermostat
entity: climate.nest
`,
config: {
type: "thermostat",
entity: "climate.nest",
},
},
{
heading: "Feature example",
config: `
- type: thermostat
entity: climate.overkiz_radiator
features:
- type: climate-hvac-modes
hvac_modes:
- heat
- 'off'
- auto
- type: climate-preset-modes
style: icons
preset_modes:
- none
- frost_protection
- eco
- comfort
- comfort-1
- comfort-2
- auto
- boost
- external
- prog
- type: climate-preset-modes
style: dropdown
preset_modes:
- none
- frost_protection
- eco
- comfort
- comfort-1
- comfort-2
- auto
- boost
- external
- prog
`,
config: {
type: "thermostat",
entity: "climate.overkiz_radiator",
features: [
{
type: "climate-hvac-modes",
hvac_modes: ["heat", "off", "auto"],
},
{
type: "climate-preset-modes",
style: "icons",
preset_modes: [
"none",
"frost_protection",
"eco",
"comfort",
"comfort-1",
"comfort-2",
"auto",
"boost",
"external",
"prog",
],
},
{
type: "climate-preset-modes",
style: "dropdown",
preset_modes: [
"none",
"frost_protection",
"eco",
"comfort",
"comfort-1",
"comfort-2",
"auto",
"boost",
"external",
"prog",
],
},
],
},
},
{
heading: "Preset only example",
config: `
- type: thermostat
entity: climate.overkiz_towel_dryer
features:
- type: climate-hvac-modes
hvac_modes:
- heat
- 'off'
- type: climate-preset-modes
style: icons
preset_modes:
- none
- frost_protection
- eco
- comfort
- comfort-1
- comfort-2
`,
config: {
type: "thermostat",
entity: "climate.overkiz_towel_dryer",
features: [
{
type: "climate-hvac-modes",
hvac_modes: ["heat", "off"],
},
{
type: "climate-preset-modes",
style: "icons",
preset_modes: [
"none",
"frost_protection",
"eco",
"comfort",
"comfort-1",
"comfort-2",
],
},
],
},
},
{
heading: "Fan only example",
config: `
- type: thermostat
entity: climate.sensibo
features:
- type: climate-hvac-modes
hvac_modes:
- fan_only
- 'off'
- type: climate-fan-modes
style: icons
fan_modes:
- low
- high
- type: climate-swing-modes
style: icons
swing_modes:
- 'both'
- 'rangefull'
- 'off'
swing_horizontal_modes:
- 'both'
- 'rangefull'
- 'off'
`,
config: {
type: "thermostat",
entity: "climate.sensibo",
features: [
{
type: "climate-hvac-modes",
hvac_modes: ["fan_only", "off"],
},
{
type: "climate-fan-modes",
style: "icons",
fan_modes: ["low", "high"],
},
{
type: "climate-swing-modes",
style: "icons",
swing_modes: ["both", "rangefull", "off"],
},
{
type: "climate-swing-horizontal-modes",
style: "icons",
swing_horizontal_modes: ["both", "rangefull", "off"],
},
],
},
},
{
heading: "Unavailable",
config: `
- type: thermostat
entity: climate.unavailable
`,
config: {
type: "thermostat",
entity: "climate.unavailable",
},
},
{
heading: "Non existing",
config: `
- type: thermostat
entity: climate.nonexisting
`,
config: {
type: "thermostat",
entity: "climate.nonexisting",
},
},
];
] satisfies DemoCardConfig<ThermostatCardConfig>[];
@customElement("demo-lovelace-thermostat-card")
class DemoThermostatEntity extends LitElement {
+114 -123
View File
@@ -1,6 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { CoverEntityFeature } from "../../../../src/data/cover";
import { LightColorMode } from "../../../../src/data/light";
import { LockEntityFeature } from "../../../../src/data/lock";
@@ -11,6 +12,7 @@ import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { ClimateEntityFeature } from "../../../../src/data/climate";
import { FanEntityFeature } from "../../../../src/data/fan";
import type { TileCardConfig } from "../../../../src/panels/lovelace/cards/types";
const ENTITIES = [
{
@@ -166,190 +168,179 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Basic example",
config: `
- type: tile
entity: switch.tv_outlet
`,
config: {
type: "tile",
entity: "switch.tv_outlet",
},
},
{
heading: "Vertical example",
config: `
- type: tile
entity: switch.tv_outlet
vertical: true
`,
config: {
type: "tile",
entity: "switch.tv_outlet",
vertical: true,
},
},
{
heading: "Custom color",
config: `
- type: tile
entity: switch.tv_outlet
color: pink
`,
config: {
type: "tile",
entity: "switch.tv_outlet",
color: "pink",
},
},
{
heading: "Whole tile tap action",
config: `
- type: tile
entity: switch.tv_outlet
color: pink
tap_action:
action: toggle
icon_tap_action:
action: none
`,
config: {
type: "tile",
entity: "switch.tv_outlet",
color: "pink",
tap_action: {
action: "toggle",
},
icon_tap_action: {
action: "none",
},
},
},
{
heading: "Unknown entity",
config: `
- type: tile
entity: light.unknown
`,
config: {
type: "tile",
entity: "light.unknown",
},
},
{
heading: "Unavailable entity",
config: `
- type: tile
entity: light.unavailable
`,
config: {
type: "tile",
entity: "light.unavailable",
},
},
{
heading: "Climate",
config: `
- type: tile
entity: climate.thermostat
`,
config: {
type: "tile",
entity: "climate.thermostat",
},
},
{
heading: "Person",
config: `
- type: tile
entity: person.paulus
`,
config: {
type: "tile",
entity: "person.paulus",
},
},
{
heading: "Light brightness feature",
config: `
- type: tile
entity: light.bed_light
features:
- type: "light-brightness"
`,
config: {
type: "tile",
entity: "light.bed_light",
features: [{ type: "light-brightness" }],
},
},
{
heading: "Light color temperature feature",
config: `
- type: tile
entity: light.bed_light
features:
- type: "color-temp"
`,
config: {
type: "tile",
entity: "light.bed_light",
features: [{ type: "light-color-temp" }],
},
},
{
heading: "Lock commands feature",
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-commands"
`,
config: {
type: "tile",
entity: "lock.front_door",
features: [{ type: "lock-commands" }],
},
},
{
heading: "Lock open door feature",
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-open-door"
`,
config: {
type: "tile",
entity: "lock.front_door",
features: [{ type: "lock-open-door" }],
},
},
{
heading: "Media player volume slider feature",
config: `
- type: tile
entity: media_player.living_room
features:
- type: "media-player-volume-slider"
`,
config: {
type: "tile",
entity: "media_player.living_room",
features: [{ type: "media-player-volume-slider" }],
},
},
{
heading: "Vacuum commands feature",
config: `
- type: tile
entity: vacuum.first_floor_vacuum
features:
- type: "vacuum-commands"
commands:
- start_pause
- stop
- return_home
`,
config: {
type: "tile",
entity: "vacuum.first_floor_vacuum",
features: [
{
type: "vacuum-commands",
commands: ["start_pause", "stop", "return_home"],
},
],
},
},
{
heading: "Cover open close feature",
config: `
- type: tile
entity: cover.kitchen_shutter
features:
- type: "cover-open-close"
`,
config: {
type: "tile",
entity: "cover.kitchen_shutter",
features: [{ type: "cover-open-close" }],
},
},
{
heading: "Cover tilt feature",
config: `
- type: tile
entity: cover.pergola_roof
features:
- type: "cover-tilt"
`,
config: {
type: "tile",
entity: "cover.pergola_roof",
features: [{ type: "cover-tilt" }],
},
},
{
heading: "Number buttons feature",
config: `
- type: tile
entity: input_number.counter
features:
- type: numeric-input
style: buttons
`,
config: {
type: "tile",
entity: "input_number.counter",
features: [{ type: "numeric-input", style: "buttons" }],
},
},
{
heading: "Dual thermostat feature",
config: `
- type: tile
entity: climate.dual_thermostat
features:
- type: target-temperature
`,
config: {
type: "tile",
entity: "climate.dual_thermostat",
features: [{ type: "target-temperature" }],
},
},
{
heading: "Fan direction feature",
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-direction
`,
config: {
type: "tile",
entity: "fan.fan_demo",
features: [{ type: "fan-direction" }],
},
},
{
heading: "Fan speed feature",
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-speed
`,
config: {
type: "tile",
entity: "fan.fan_demo",
features: [{ type: "fan-speed" }],
},
},
{
heading: "Fan oscillate feature",
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-oscillate
`,
config: {
type: "tile",
entity: "fan.fan_demo",
features: [{ type: "fan-oscillate" }],
},
},
];
] satisfies DemoCardConfig<TileCardConfig>[];
@customElement("demo-lovelace-tile-card")
class DemoTile extends LitElement {
+12 -10
View File
@@ -4,6 +4,8 @@ import { customElement, query } from "lit/decorators";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { mockTodo } from "../../../../demo/src/stubs/todo";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { TodoListCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import "../../components/demo-cards";
const ENTITIES = [
@@ -27,20 +29,20 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "List example",
config: `
- type: todo-list
entity: todo.shopping_list
`,
config: {
type: "todo-list",
entity: "todo.shopping_list",
},
},
{
heading: "List with title example",
config: `
- type: todo-list
title: Shopping List
entity: todo.read_only
`,
config: {
type: "todo-list",
title: "Shopping List",
entity: "todo.read_only",
},
},
];
] satisfies DemoCardConfig<TodoListCardConfig>[];
@customElement("demo-lovelace-todo-list-card")
class DemoTodoListEntity extends LitElement {
+2 -3
View File
@@ -147,8 +147,8 @@
"@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",
"@octokit/plugin-retry": "8.1.0",
"@octokit/auth-oauth-device": "8.0.4",
"@octokit/plugin-retry": "8.1.1",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.62.1",
"@rsdoctor/rspack-plugin": "1.6.1",
@@ -188,7 +188,6 @@
"glob": "13.0.6",
"globals": "17.8.0",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
+2 -4
View File
@@ -1,9 +1,8 @@
import type { ReactiveElement } from "lit";
import { getHistoryState, updateHistoryState } from "../navigate";
import { throttle } from "../util/throttle";
const throttleReplaceState = throttle((value) => {
updateHistoryState({ scrollPosition: value });
history.replaceState({ scrollPosition: value }, "");
}, 300);
export function restoreScroll(selector: string) {
@@ -40,8 +39,7 @@ export function restoreScroll(selector: string) {
newDescriptor = {
get(this: ReactiveElement) {
return (
this[`__${String(propertyKey)}`] ||
getHistoryState()?.scrollPosition
this[`__${String(propertyKey)}`] || history.state?.scrollPosition
);
},
set(this: ReactiveElement, value) {
+39 -77
View File
@@ -1,7 +1,6 @@
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
@@ -18,32 +17,6 @@ export interface NavigateOptions {
// max time to wait for dialogs to close before navigating
const DIALOG_WAIT_TIMEOUT = 500;
/**
* State of the current history entry. Always read through this, the app writes
* to the main window and a panel running in an iframe has its own history.
*/
export const getHistoryState = (): any => mainWindow.history.state;
/**
* 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
@@ -51,7 +24,10 @@ export const replaceCurrentUrl = (url: string) => {
* The current URL is not changed.
*/
export const setRefreshUrl = (path: string) => {
updateHistoryState({ refreshUrl: path });
mainWindow.history.replaceState(
{ ...mainWindow.history.state, refreshUrl: path },
""
);
};
/**
@@ -87,44 +63,37 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
}
const replace = options?.replace || false;
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 },
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),
"",
path
);
} else {
const { data } = options ?? {};
// Only objects survive being merged with `from`. Callers outside the app
// pass whatever they want, so keep anything else under a key rather than
// letting the spread mangle it.
const mergeable =
data === undefined || (typeof data === "object" && data !== null);
history.pushState(
mergeable
? { ...data, from: currentPath() }
: { data, from: currentPath() },
"",
path
);
mainWindow.history.pushState(options?.data ?? null, "", path);
}
fireEvent(mainWindow, "location-changed", {
replace,
});
@@ -132,17 +101,8 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
};
/**
* 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).
* 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.
*/
export const goBack = async (fallbackPath?: string): Promise<void> => {
const canProceed = await ensureDialogsClosed(Date.now());
@@ -150,12 +110,14 @@ export const goBack = async (fallbackPath?: string): Promise<void> => {
return;
}
// 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();
// Check if we have history to go back to
const { history } = mainWindow;
if (history.length > 1) {
history.back();
return;
}
await navigate(fallbackPath || "/", { replace: true });
// No history available, navigate to fallback path
const fallback = fallbackPath || "/";
navigate(fallback, { replace: true });
};
+13
View File
@@ -0,0 +1,13 @@
// RFC 1123 hostname label: 1-63 chars, alphanumeric, with hyphens allowed
// only between the first and last character. The hyphen is escaped because a
// `pattern` attribute is compiled with the `v` flag, under which a trailing
// unescaped hyphen is an invalid character class — and a pattern that fails to
// compile is silently ignored, disabling validation altogether.
const LABEL = "[a-zA-Z0-9](?:[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?";
// Hostname such as "localhost" or "homeassistant.lan", as dot-separated
// labels. Unanchored, for use as an HTML `pattern` attribute (the browser
// anchors it as `^(?:…)$`). The final label may not be all digits, so a
// mistyped IP address like "300.1.1.1" is rejected rather than accepted as a
// hostname. Deliberately excludes underscores and a trailing dot.
export const HOSTNAME_PATTERN = `(?:${LABEL}\\.)*(?!\\d+$)${LABEL}`;
-10
View File
@@ -1,10 +0,0 @@
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;
+5 -6
View File
@@ -53,8 +53,10 @@ export class HaControlNumberButton extends LitElement {
});
private _boundedValue(value: number) {
const clamped = conditionalClamp(value, this.min, this.max);
return Math.round(clamped / this._step) * this._step;
// Clamp after snapping: when the step does not divide the range evenly,
// snapping alone rounds past the bounds (min 1, max 99, step 10 → 0 / 100).
const stepped = Math.round(value / this._step) * this._step;
return conditionalClamp(stepped, this.min, this.max);
}
private get _step() {
@@ -67,10 +69,7 @@ export class HaControlNumberButton extends LitElement {
private get _tenPercentStep() {
if (this.max == null || this.min == null) return this._step;
const range = this.max - this.min / 10;
if (range <= this._step) return this._step;
return Math.max(range / 10);
return Math.max((this.max - this.min) / 10, this._step);
}
private _handlePlusButton() {
+5 -7
View File
@@ -115,7 +115,9 @@ export class HaControlSlider extends LitElement {
}
steppedValue(value: number) {
return Math.round(value / this.step) * this.step;
// Clamp after snapping: when the step does not divide the range evenly,
// snapping alone rounds past the bounds (min 1, max 99, step 10 → 0 / 100).
return this.boundedValue(Math.round(value / this.step) * this.step);
}
private _displayedValue(value: number) {
@@ -254,13 +256,9 @@ export class HaControlSlider extends LitElement {
} else if (e.code === "End") {
this.value = this.max;
} else if (e.code === "PageUp") {
this.value = this.steppedValue(
this.boundedValue((this.value ?? 0) + this._tenPercentStep)
);
this.value = this.steppedValue((this.value ?? 0) + this._tenPercentStep);
} else if (e.code === "PageDown") {
this.value = this.steppedValue(
this.boundedValue((this.value ?? 0) - this._tenPercentStep)
);
this.value = this.steppedValue((this.value ?? 0) - this._tenPercentStep);
} else {
const isRtl = mainWindow.document.dir === "rtl";
let multiplier = 1;
@@ -131,6 +131,7 @@ export class HaNumberSelector extends LitElement {
.hint=${isBox ? this.helper : undefined}
.disabled=${this.disabled}
.required=${this.required}
.validationMessage=${this.selector.number?.validation_message}
type="number"
autoValidate
.withoutSpinButtons=${!isBox}
+27 -3
View File
@@ -31,6 +31,8 @@ export class HaToast extends LitElement {
@property({ type: Number, attribute: "bottom-offset" }) public bottomOffset =
0;
@property({ type: Boolean }) public stacked = false;
@query(".toast")
private _toast?: HTMLDivElement;
@@ -148,7 +150,12 @@ export class HaToast extends LitElement {
}
private _showToastPopover(): void {
if (!this._toast || !popoverSupported || this._isPopoverOpen()) {
if (
!this._toast ||
this.stacked ||
!popoverSupported ||
this._isPopoverOpen()
) {
return;
}
@@ -156,7 +163,12 @@ export class HaToast extends LitElement {
}
private _hideToastPopover(): void {
if (!this._toast || !popoverSupported || !this._isPopoverOpen()) {
if (
!this._toast ||
this.stacked ||
!popoverSupported ||
!this._isPopoverOpen()
) {
return;
}
@@ -187,12 +199,15 @@ export class HaToast extends LitElement {
class=${classMap({
toast: true,
active: this._active,
stacked: this.stacked,
visible: this._visible,
})}
style=${styleMap({
"--ha-toast-bottom-offset": `${this.bottomOffset}px`,
})}
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
popover=${ifDefined(
popoverSupported && !this.stacked ? "manual" : undefined
)}
>
<span class="message">${this.labelText}</span>
<div class=${classMap({ actions: true, "has-action": hasAction })}>
@@ -253,6 +268,15 @@ export class HaToast extends LitElement {
transform: translate(calc(-50% * var(--scale-direction)), 0);
}
.toast.stacked {
position: static;
transform: translateY(var(--ha-space-2));
}
.toast.stacked.visible {
transform: translateY(0);
}
.toast:not(.active) {
display: none;
}
+3
View File
@@ -397,6 +397,9 @@ export interface NumberSelector {
unit_of_measurement?: string;
slider_ticks?: boolean;
translation_key?: string;
// Shown instead of the browser's native message when the value fails
// min/max/step constraint validation.
validation_message?: string;
} | null;
}
+10 -10
View File
@@ -40,11 +40,7 @@ import {
getEntityEntryContext,
} from "../../common/entity/context/get_entity_context";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import {
getHistoryState,
navigate,
updateHistoryState,
} from "../../common/navigate";
import { navigate } from "../../common/navigate";
import type { LocalizeKeys } from "../../common/translations/localize";
import { computeRTL } from "../../common/util/compute_rtl";
import { withViewTransition } from "../../common/util/view-transition";
@@ -272,12 +268,16 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
}
private _setView(view: MoreInfoView) {
updateHistoryState({
dialogParams: {
...getHistoryState()?.dialogParams,
view,
history.replaceState(
{
...history.state,
dialogParams: {
...history.state?.dialogParams,
view,
},
},
});
""
);
this._currView = view;
}
-29
View File
@@ -1,29 +0,0 @@
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { goBack } from "../common/navigate";
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
/**
* Shared behavior of the toolbar back arrow. The arrow is a link to the
* declared parent page so it can be opened in a new tab, but a plain click
* returns to the page the user came from instead.
*/
export const handleBackClick = (
ev: MouseEvent,
backPath?: string,
backCallback?: () => void
): void => {
const path = sanitizeNavigationPath(backPath);
// Middle click, ctrl and cmd open the parent in a new tab, let the anchor
// handle those.
if (path && !isNavigationClick(ev)) {
return;
}
if (backCallback) {
backCallback();
return;
}
goBack(path);
};
+19 -10
View File
@@ -4,8 +4,8 @@ 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 { goBack } from "../common/navigate";
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
import { handleBackClick } from "./back-navigation";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import { haStyleScrollbar } from "../resources/styles";
@@ -37,14 +37,19 @@ class HassSubpage extends LitElement {
<div class="toolbar ${classMap({ narrow: this.narrow })}">
<div class="toolbar-content">
${
this.mainPage || (!backPath && history.state?.root)
this.mainPage || history.state?.root
? html`<ha-menu-button></ha-menu-button>`
: html`
<ha-icon-button-arrow-prev
.href=${backPath}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
: 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>
`
}
<div class="main-title">
@@ -74,8 +79,12 @@ class HassSubpage extends LitElement {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
}
private _backTapped(ev: MouseEvent): void {
handleBackClick(ev, this.backPath, this.backCallback);
private _backTapped(): void {
if (this.backCallback) {
this.backCallback();
return;
}
goBack();
}
static get styles(): CSSResultGroup {
+18 -10
View File
@@ -14,10 +14,9 @@ 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 { navigate } from "../common/navigate";
import { goBack, navigate } from "../common/navigate";
import type { LocalizeFunc } from "../common/translations/localize";
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
import { handleBackClick } from "./back-navigation";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import "../components/ha-svg-icon";
@@ -176,12 +175,17 @@ export class HassTabsSubpage extends LitElement {
${
this.mainPage || (!backPath && history.state?.root)
? html`<ha-menu-button></ha-menu-button>`
: html`
<ha-icon-button-arrow-prev
.href=${backPath}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
: 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>
`
}
${
this._narrow || !this.showTabs
@@ -242,8 +246,12 @@ export class HassTabsSubpage extends LitElement {
this._content.focus({ preventScroll: true });
}
private _backTapped(ev: MouseEvent): void {
handleBackClick(ev, this.backPath, this.backCallback);
private _backTapped(): void {
if (this.backCallback) {
this.backCallback();
return;
}
goBack();
}
private _isActiveTabPath(tabPath: string, currentPath: string): boolean {
+220 -81
View File
@@ -1,7 +1,20 @@
import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { css, html, LitElement, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import {
customElement,
property,
query,
queryAll,
state,
} from "lit/decorators";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../common/dom/fire_event";
import { popoverSupported } from "../common/feature-detect/support-popover";
import type { LocalizeKeys } from "../common/translations/localize";
import "../components/ha-button";
import "../components/ha-icon-button";
@@ -10,7 +23,7 @@ import type { ToastClosedEventDetail } from "../components/ha-toast";
import type { HomeAssistant } from "../types";
export interface ShowToastParams {
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
// Unique ID for updating or closing a specific toast without flickering.
id?: string;
message:
| string
@@ -34,105 +47,150 @@ export interface ToastActionParams {
| { translationKey: LocalizeKeys; args?: Record<string, string | number> };
}
interface Notification {
key: string;
parameters: ShowToastParams;
}
@customElement("notification-manager")
class NotificationManager extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _parameters?: ShowToastParams;
@state() private _notifications: Notification[] = [];
@query("ha-toast")
private _toast!: HTMLElementTagNameMap["ha-toast"] | undefined;
@query(".stack") private _stack?: HTMLDivElement;
private _showDialogId = 0;
@queryAll("ha-toast")
private _toasts!: NodeListOf<HTMLElementTagNameMap["ha-toast"]>;
private _anonymousId = 0;
public async showDialog(parameters: ShowToastParams) {
const showId = ++this._showDialogId;
if (!parameters.id || this._parameters?.id !== parameters.id) {
await this._toast?.hide();
}
if (showId !== this._showDialogId) {
return;
}
if (parameters.duration === 0) {
this._parameters = undefined;
if (parameters.id) {
await this._closeNotification(`identified-${parameters.id}`);
} else {
const notification = [...this._notifications]
.reverse()
.find(({ parameters: { id } }) => !id);
if (notification) {
await this._closeNotification(notification.key);
}
}
return;
}
this._parameters = parameters;
const normalizedParameters = {
...parameters,
duration:
parameters.duration === undefined ||
(parameters.duration > 0 && parameters.duration <= 4000)
? 4000
: parameters.duration,
};
const key = parameters.id
? `identified-${parameters.id}`
: `anonymous-${++this._anonymousId}`;
const existingIndex = parameters.id
? this._notifications.findIndex(
(notification) => notification.key === key
)
: -1;
if (
this._parameters.duration === undefined ||
(this._parameters.duration > 0 && this._parameters.duration <= 4000)
) {
this._parameters.duration = 4000;
}
this._notifications =
existingIndex === -1
? [...this._notifications, { key, parameters: normalizedParameters }]
: this._notifications.map((notification, index) =>
index === existingIndex
? { key, parameters: normalizedParameters }
: notification
);
await this.updateComplete;
if (showId !== this._showDialogId) {
return;
}
this._toast?.show();
this._showStack();
this._getToast(key)?.show();
}
private _toastClosed(ev: HASSDomEvent<ToastClosedEventDetail>) {
private _toastClosed(
ev: HASSDomEvent<ToastClosedEventDetail> &
HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-toast"]>
) {
const key = ev.currentTarget.dataset.notificationKey!;
if (ev.detail.reason === "dismiss") {
this._parameters?.dismiss?.();
this._getNotification(key)?.parameters.dismiss?.();
}
this._parameters = undefined;
this._removeNotification(key);
}
protected render() {
if (!this._parameters) {
if (!this._notifications.length) {
return nothing;
}
const bottomOffset = Math.max(
...this._notifications.map(
({ parameters }) => parameters.bottomOffset ?? 0
)
);
return html`
<ha-toast
.labelText=${
typeof this._parameters.message !== "string"
? this.hass.localize(
this._parameters.message.translationKey,
this._parameters.message.args
)
: this._parameters.message
}
.announceText=${
this._parameters.announceMessage
? typeof this._parameters.announceMessage !== "string"
? this.hass.localize(
this._parameters.announceMessage.translationKey,
this._parameters.announceMessage.args
)
: this._parameters.announceMessage
: undefined
}
.timeoutMs=${this._parameters.duration!}
.bottomOffset=${this._parameters.bottomOffset ?? 0}
@toast-closed=${this._toastClosed}
<div
class="stack"
style=${styleMap({
"--notification-stack-bottom-offset": `${bottomOffset}px`,
})}
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
>
${this._renderAction(this._parameters.secondaryAction, true)}
${this._renderAction(this._parameters.action, false)}
${
this._parameters?.dismissable
? html`
<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
slot="dismiss"
@click=${this._dismissClicked}
></ha-icon-button>
`
: nothing
}
</ha-toast>
${repeat(
this._notifications,
(notification) => notification.key,
({ key, parameters }) => html`
<ha-toast
data-notification-key=${key}
.labelText=${
typeof parameters.message !== "string"
? this.hass.localize(
parameters.message.translationKey,
parameters.message.args
)
: parameters.message
}
.announceText=${
parameters.announceMessage
? typeof parameters.announceMessage !== "string"
? this.hass.localize(
parameters.announceMessage.translationKey,
parameters.announceMessage.args
)
: parameters.announceMessage
: undefined
}
.timeoutMs=${parameters.duration!}
.stacked=${true}
@toast-closed=${this._toastClosed}
>
${this._renderAction(key, parameters.secondaryAction, true)}
${this._renderAction(key, parameters.action, false)}
${
parameters.dismissable
? html`
<ha-icon-button
data-notification-key=${key}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
slot="dismiss"
@click=${this._dismissClicked}
></ha-icon-button>
`
: nothing
}
</ha-toast>
`
)}
</div>
`;
}
private _renderAction(
key: string,
action: ToastActionParams | undefined,
secondary: boolean
) {
@@ -141,6 +199,7 @@ class NotificationManager extends LitElement {
}
return html`
<ha-button
data-notification-key=${key}
appearance=${action.primary ? "filled" : "plain"}
size="s"
slot="action"
@@ -155,19 +214,99 @@ class NotificationManager extends LitElement {
`;
}
private _buttonClicked() {
this._toast?.hide("action");
this._parameters?.action?.action();
private _buttonClicked(
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-button"]>
) {
const key = ev.currentTarget.dataset.notificationKey!;
this._getToast(key)?.hide("action");
this._getNotification(key)?.parameters.action?.action();
}
private _secondaryButtonClicked() {
this._toast?.hide("action");
this._parameters?.secondaryAction?.action();
private _secondaryButtonClicked(
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-button"]>
) {
const key = ev.currentTarget.dataset.notificationKey!;
this._getToast(key)?.hide("action");
this._getNotification(key)?.parameters.secondaryAction?.action();
}
private _dismissClicked() {
this._toast?.hide("dismiss");
private _dismissClicked(
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-icon-button"]>
) {
this._getToast(ev.currentTarget.dataset.notificationKey!)?.hide("dismiss");
}
private _getNotification(key: string) {
return this._notifications.find((notification) => notification.key === key);
}
private _getToast(key: string) {
return [...this._toasts].find(
(toast) => toast.dataset.notificationKey === key
);
}
private async _closeNotification(key: string) {
const notification = this._getNotification(key);
if (!notification) {
return;
}
await this._getToast(key)?.hide();
if (this._getNotification(key) === notification) {
this._removeNotification(key);
}
}
private _removeNotification(key: string) {
this._notifications = this._notifications.filter(
(notification) => notification.key !== key
);
if (!this._notifications.length) {
this._hideStack();
}
}
private _showStack() {
if (!popoverSupported || !this._stack) {
return;
}
// Top-layer order is order of entry — re-enter so we paint above any
// dialog backdrop opened since the stack was first shown.
if (this._stack.matches(":popover-open")) {
this._stack.hidePopover();
}
this._stack.showPopover();
}
private _hideStack() {
if (!popoverSupported || !this._stack?.matches(":popover-open")) {
return;
}
this._stack.hidePopover();
}
static override styles = css`
.stack {
position: fixed;
inset-block-start: auto;
inset-inline-end: auto;
inset-block-end: calc(
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4) +
var(--notification-stack-bottom-offset, 0px)
);
inset-inline-start: 50%;
margin: 0;
padding: 0;
border: none;
overflow: visible;
background: transparent;
transform: translateX(calc(-50% * var(--scale-direction)));
display: flex;
flex-direction: column;
align-items: center;
gap: var(--ha-space-2);
}
`;
}
declare global {
@@ -3,6 +3,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 { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
@@ -455,7 +456,7 @@ class DialogAreaDetail
return deviceReg && deviceReg.area_id === areaId;
};
private _nameChanged(ev: InputEvent) {
private _nameChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
this._error = undefined;
this._name = (ev.target as HaInput).value ?? "";
this._updateDirtyState(this._currentState());
@@ -479,7 +480,9 @@ class DialogAreaDetail
this._updateDirtyState(this._currentState());
}
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
private _pictureChanged(
ev: ValueChangedEvent<string | null> & HASSDomTargetEvent<HaPictureUpload>
) {
this._error = undefined;
this._picture = (ev.target as HaPictureUpload).value;
this._updateDirtyState(this._currentState());
@@ -490,7 +493,9 @@ class DialogAreaDetail
this._updateDirtyState(this._currentState());
}
private _sensorChanged(ev: CustomEvent): void {
private _sensorChanged(
ev: ValueChangedEvent<string> & HASSDomTargetEvent<HaEntityPicker>
): void {
const deviceClass = (ev.target as HaEntityPicker).includeDeviceClasses![0];
const key = `_${deviceClass}Entity`;
this[key] = ev.detail.value || null;
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import "../../../components/chips/ha-chip-set";
import "../../../components/chips/ha-input-chip";
import "../../../components/ha-alert";
@@ -336,13 +337,13 @@ class DialogFloorDetail extends DirtyStateProviderMixin<FloorFormState>()(
this._updateDirtyState(this._currentState());
}
private _nameChanged(ev: InputEvent) {
private _nameChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
this._error = undefined;
this._name = (ev.target as HaInput).value ?? "";
this._updateDirtyState(this._currentState());
}
private _levelChanged(ev: InputEvent) {
private _levelChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
this._error = undefined;
this._level =
(ev.target as HaInput).value === ""
@@ -644,7 +644,6 @@ 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,6 +86,8 @@ export class HaConfigAreasDashboard extends LitElement {
@state() private _hierarchy?: AreasFloorHierarchy;
private _searchParms = new URLSearchParams(window.location.search);
private _blockHierarchyUpdate = false;
private _blockHierarchyUpdateTimeout?: number;
@@ -166,7 +168,9 @@ export class HaConfigAreasDashboard extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.isWide=${this.isWide}
back-path="/config"
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
.tabs=${configSections.areas}
.route=${this.route}
has-fab
@@ -443,7 +443,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/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, replaceCurrentUrl } from "../../../common/navigate";
import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
@@ -110,7 +110,6 @@ export class HaAutomationTrace extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/automation/dashboard"
.header=${title}
.scrollable=${this.narrow}
>
@@ -453,7 +452,11 @@ export class HaAutomationTrace extends LitElement {
if (runId) {
const params = new URLSearchParams(location.search);
params.delete("run_id");
replaceCurrentUrl(`${location.pathname}?${params.toString()}`);
history.replaceState(
null,
"",
`${location.pathname}?${params.toString()}`
);
}
await showAlertDialog(this, {
@@ -11,7 +11,6 @@ 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,
@@ -36,6 +35,8 @@ import type HaAutomationSidebar from "./ha-automation-sidebar";
export const SIDEBAR_DEFAULT_WIDTH = 500;
export const PASTED_CONFIG_TOAST_ID = "pasted-config";
export const ManualEditorMixin = <TConfig>(
superClass: Constructor<LitElement>
) => {
@@ -172,7 +173,11 @@ export const ManualEditorMixin = <TConfig>(
}
protected clearParam(param: string) {
replaceCurrentUrl(constructUrlCurrentPath(removeSearchParam(param)));
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
}
protected async openSidebar(ev: CustomEvent<SidebarConfig>) {
@@ -234,6 +239,7 @@ export const ManualEditorMixin = <TConfig>(
this.pastedConfig = undefined;
showToast(this, {
id: PASTED_CONFIG_TOAST_ID,
message: "",
duration: 0,
});
@@ -37,7 +37,10 @@ import "./action/ha-automation-action";
import type HaAutomationAction from "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import type HaAutomationCondition from "./condition/ha-automation-condition";
import { ManualEditorMixin } from "./ha-manual-editor-mixin";
import {
ManualEditorMixin,
PASTED_CONFIG_TOAST_ID,
} from "./ha-manual-editor-mixin";
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
import { manualEditorStyles, saveFabStyles } from "./styles";
import "./trigger/ha-automation-trigger";
@@ -431,6 +434,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
protected showPastedToastWithUndo() {
showEditorToast(this, {
id: PASTED_CONFIG_TOAST_ID,
message: this.hass.localize(
"ui.panel.config.automation.editor.paste_toast_message"
),
@@ -74,6 +74,8 @@ 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) {
@@ -204,7 +206,9 @@ class HaConfigBackupOverview extends LitElement {
return html`
<hass-subpage
back-path="/config/system"
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config/system"
}
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.backup.overview.header")}
@@ -4,7 +4,6 @@ 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";
@@ -119,7 +118,7 @@ class HaConfigBackupSettings extends LitElement {
}
private _clearHash() {
replaceCurrentUrl(window.location.pathname);
history.replaceState(null, "", window.location.pathname);
}
protected render() {
@@ -2,6 +2,7 @@ import { mdiClose, mdiOpenInNew } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import { withViewTransition } from "../../../common/util/view-transition";
import "../../../components/ha-alert";
import "../../../components/ha-button";
@@ -279,7 +280,7 @@ class DialogImportBlueprint extends DirtyStateProviderMixin<BlueprintImportState
});
}
private _inputChanged(ev: Event) {
private _inputChanged(ev: HASSDomTargetEvent<HaInput>) {
this._updateDirtyState({
value: (ev.target as HaInput).value ?? "",
hasResult: !!this._result,
@@ -13,7 +13,10 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
@@ -483,7 +486,7 @@ class HaBlueprintOverview extends LitElement {
this._createNew(blueprint);
}
private _handleUsageClick = (ev: Event) => {
private _handleUsageClick = (ev: HASSDomCurrentTargetEvent<HTMLElement>) => {
ev.stopPropagation();
ev.preventDefault();
const target = ev.currentTarget as HTMLElement | null;
@@ -68,6 +68,7 @@ export class CloudAccount extends SubscribeMixin(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}>
@@ -21,7 +21,6 @@ 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,7 +45,6 @@ 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,7 +38,6 @@ 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,6 +66,8 @@ 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;
@@ -153,7 +155,9 @@ class HaConfigSectionUpdates extends LitElement {
return html`
<hass-subpage
back-path="/config/system"
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config/system"
}
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.updates.caption")}
@@ -986,7 +986,6 @@ 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, updateHistoryState } from "../../../common/navigate";
import { navigate } from "../../../common/navigate";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
@@ -778,7 +778,9 @@ export class HaConfigDeviceDashboard extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
.tabs=${configSections.devices}
.route=${this.route}
.searchLabel=${this.hass.localize(
@@ -1041,7 +1043,7 @@ export class HaConfigDeviceDashboard extends LitElement {
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
updateHistoryState({ filter: this._filter });
history.replaceState({ filter: this._filter }, "");
}
private _addDevice() {
@@ -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 { HASSDomEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
@@ -340,7 +341,7 @@ export class DialogEnergyBatterySettings
}
private _handlePowerConfigChanged(
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
ev: HASSDomEvent<HASSDomEvents["power-config-changed"]>
) {
this._powerType = ev.detail.powerType;
this._powerConfig = ev.detail.powerConfig;
@@ -3,6 +3,7 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
@@ -214,7 +215,7 @@ export class DialogEnergyCustomise
`;
}
private _toggleCard = (ev: Event): void => {
private _toggleCard = (ev: HASSDomCurrentTargetEvent<HaSwitch>): void => {
const target = ev.currentTarget as HaSwitch;
const cardKey = target.dataset.cardKey;
if (!cardKey) {
@@ -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 { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
@@ -353,7 +354,7 @@ export class DialogEnergyGasSettings
`;
}
private _handleCostChanged(ev: Event) {
private _handleCostChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
this._updateFormDirtyState();
}
@@ -2,6 +2,10 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
@@ -593,7 +597,9 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _handleImportCostTypeChanged(ev: Event) {
private _handleImportCostTypeChanged(
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
) {
this._importCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
// Clear other cost fields when switching types
this._source = {
@@ -605,7 +611,9 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _handleExportCostTypeChanged(ev: Event) {
private _handleExportCostTypeChanged(
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
) {
this._exportCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
// Clear other cost fields when switching types
this._source = {
@@ -630,9 +638,10 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _numberCostChanged(ev: Event) {
const input = ev.currentTarget as HTMLInputElement;
const value = input.value ? parseFloat(input.value) : null;
private _numberCostChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
const value = ev.currentTarget.value
? parseFloat(ev.currentTarget.value)
: null;
this._source = { ...this._source!, number_energy_price: value };
this._updateFormDirtyState();
}
@@ -653,15 +662,16 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _numberCompensationChanged(ev: Event) {
const input = ev.currentTarget as HTMLInputElement;
const value = input.value ? parseFloat(input.value) : null;
private _numberCompensationChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
const value = ev.currentTarget.value
? parseFloat(ev.currentTarget.value)
: null;
this._source = { ...this._source!, number_energy_price_export: value };
this._updateFormDirtyState();
}
private _handlePowerConfigChanged(
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
ev: HASSDomEvent<HASSDomEvents["power-config-changed"]>
) {
this._powerType = ev.detail.powerType;
this._powerConfig = ev.detail.powerConfig;
@@ -3,6 +3,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 { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-checkbox";
@@ -12,6 +13,7 @@ import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-svg-icon";
import "../../../../components/radio/ha-radio-group";
import "../../../../components/input/ha-input";
import { domainToName } from "../../../../data/integration";
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
import "../../../../components/radio/ha-radio-option";
import type { ConfigEntry } from "../../../../data/config_entries";
@@ -234,7 +236,7 @@ export class DialogEnergySolarSettings
},
this.hass.auth.data.hassUrl
)}
/>${entry.title}
/>${entry.title || domainToName(this.hass.localize, entry.domain)}
</div>
</ha-checkbox>`
)}
@@ -290,7 +292,7 @@ export class DialogEnergySolarSettings
);
}
private _handleForecastChanged(ev: Event) {
private _handleForecastChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
this._forecast = (ev.currentTarget as HaRadioGroup).value === "true";
this._updateFormDirtyState();
}
@@ -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 { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
@@ -289,7 +290,7 @@ export class DialogEnergyWaterSettings
`;
}
private _handleCostChanged(ev: Event) {
private _handleCostChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
this._updateFormDirtyState();
}
@@ -3,6 +3,7 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/entity/ha-statistic-picker";
@@ -234,7 +235,7 @@ export class HaEnergyPowerConfig extends LitElement {
fireEvent(this, "hass-more-info", { entityId: this.helperEntityId! });
}
private _handlePowerTypeChanged(ev: Event) {
private _handlePowerTypeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
const newPowerType = (ev.currentTarget as HaRadioGroup).value as PowerType;
// Clear power config when switching types
fireEvent(this, "power-config-changed", {
+7 -1
View File
@@ -78,6 +78,8 @@ 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;
@@ -124,7 +126,11 @@ class HaConfigEnergy extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
back-path="/config/lovelace/dashboards"
.backPath=${
this._searchParms.has("historyBack")
? undefined
: "/config/lovelace/dashboards"
}
.route=${this.route}
.tabs=${TABS}
>
@@ -39,7 +39,6 @@ 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 {
@@ -811,7 +810,9 @@ export class HaConfigEntities extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
.route=${this.route}
.tabs=${configSections.devices}
.columns=${this._columns(this.hass.localize, filteredEntities)}
@@ -1245,7 +1246,7 @@ export class HaConfigEntities extends LitElement {
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
updateHistoryState({ filter: this._filter });
history.replaceState({ filter: this._filter }, "");
}
private _handleSelectionChanged(
+1
View File
@@ -106,6 +106,7 @@ class HaPanelConfig extends HassRouterPage {
integrations: {
tag: "ha-config-integrations",
load: () => import("./integrations/ha-config-integrations"),
waitForReady: true,
},
labels: {
tag: "ha-config-labels",
@@ -4,6 +4,9 @@ import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
import "../../../../components/radio/ha-radio-group";
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
import "../../../../components/radio/ha-radio-option";
import type { InputBoolean } from "../../../../data/input_boolean";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -22,6 +25,8 @@ class HaInputBooleanForm extends LitElement {
@state() private _icon!: string;
@state() private _initial?: boolean;
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
set item(item: InputBoolean) {
@@ -29,9 +34,11 @@ class HaInputBooleanForm extends LitElement {
if (item) {
this._name = item.name || "";
this._icon = item.icon || "";
this._initial = item.initial;
} else {
this._name = "";
this._icon = "";
this._initial = undefined;
}
}
@@ -70,10 +77,58 @@ class HaInputBooleanForm extends LitElement {
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-radio-group
.label=${this.hass.localize(
"ui.dialogs.helper_settings.input_boolean.initial"
)}
.value=${
this._initial === undefined
? "restore"
: this._initial
? "on"
: "off"
}
.disabled=${this.disabled}
name="initial"
@change=${this._initialChanged}
>
<ha-radio-option value="restore">
${this.hass.localize(
"ui.dialogs.helper_settings.input_boolean.restore"
)}
</ha-radio-option>
<ha-radio-option value="on">
${this.hass.localize(
"ui.dialogs.helper_settings.input_boolean.turn_on"
)}
</ha-radio-option>
<ha-radio-option value="off">
${this.hass.localize(
"ui.dialogs.helper_settings.input_boolean.turn_off"
)}
</ha-radio-option>
</ha-radio-group>
</div>
`;
}
private _initialChanged(ev: Event) {
const option = String((ev.currentTarget as HaRadioGroup).value);
const initial = option === "restore" ? undefined : option === "on";
if (initial === this._initial) {
return;
}
const newValue = { ...this._item } as InputBoolean;
if (initial === undefined) {
delete newValue.initial;
} else {
newValue.initial = initial;
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
private _valueChanged(ev: CustomEvent) {
if (!this.new && !this._item) {
return;
@@ -108,6 +163,9 @@ class HaInputBooleanForm extends LitElement {
ha-input {
margin: var(--ha-space-2) 0;
}
ha-radio-group {
margin-top: var(--ha-space-5);
}
`,
];
}
@@ -644,7 +644,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
.route=${this.route}
.tabs=${configSections.devices}
.searchLabel=${this.hass.localize(
@@ -373,11 +373,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
: this._manifest?.documentation;
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/integrations/dashboard"
>
<hass-subpage .hass=${this.hass} .narrow=${this.narrow}>
${
documentationLink
? html`
@@ -1,4 +1,5 @@
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
import { consume } from "@lit/context";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
@@ -13,7 +14,7 @@ import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { navigate, updateHistoryState } from "../../../common/navigate";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { extractSearchParam } from "../../../common/url/search-params";
import { nextRender } from "../../../common/util/render-status";
@@ -54,6 +55,10 @@ import type { ImprovDiscoveredDevice } from "../../../external_app/external_mess
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import type { HassTabsSubpage } from "../../../layouts/hass-tabs-subpage";
import {
childPanelReadyContext,
type RegisterChildPanelReady,
} from "../../../layouts/panel-ready";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
@@ -158,6 +163,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
window.location.hash.substring(1)
);
@state() private _searchParams = new URLSearchParams(window.location.search);
@state() private _filter: string = history.state?.filter || "";
@state() private _logInfos?: Record<string, IntegrationLogInfo>;
@@ -166,6 +173,17 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
@query("hass-tabs-subpage") private _tabsSubpage?: HassTabsSubpage;
private _resolveInitialRender?: () => void;
private _initialRenderComplete = new Promise<void>((resolve) => {
this._resolveInitialRender = resolve;
});
private _childReadyRegistered = false;
@consume({ context: childPanelReadyContext, subscribe: true })
private _registerChildPanelReady?: RegisterChildPanelReady;
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener(
@@ -386,6 +404,10 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
protected updated(changed: PropertyValues<this>) {
super.updated(changed);
if (!this._childReadyRegistered && this._registerChildPanelReady) {
this._registerChildPanelReady(this._initialRenderComplete);
this._childReadyRegistered = true;
}
if (changed.has("route")) {
this._handleRouteChanged();
}
@@ -413,6 +435,9 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
}
if (this.configEntries && this.configEntriesInProgress) {
this._resolveInitialRender?.();
this._resolveInitialRender = undefined;
const activeElement = deepActiveElement();
if (
@@ -501,7 +526,9 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
return html`
<hass-tabs-subpage
.hass=${this.hass}
back-path="/config"
.backPath=${
this._searchParams.has("historyBack") ? undefined : "/config"
}
.route=${this.route}
.tabs=${configSections.devices}
has-fab
@@ -858,7 +885,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
private _handleSearchChange(ev: InputEvent) {
this._filter = (ev.target as HaInputSearch).value ?? "";
updateHistoryState({ filter: this._filter });
history.replaceState({ filter: this._filter }, "");
}
private async _highlightEntry() {
@@ -11,6 +11,7 @@ import {
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-loading-screen";
import { ChildPanelReady } from "../../../layouts/panel-ready";
import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@@ -70,6 +71,11 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
private _loadTranslationsPromise?: Promise<LocalizeFunc>;
public constructor() {
super();
new ChildPanelReady(this);
}
public hassSubscribe() {
return [
subscribeConfigEntries(
@@ -3,6 +3,7 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
@@ -451,7 +452,9 @@ export class BluetoothAdapterInfoPage extends LitElement {
return this._formatModeLabel(scannerState.current_mode);
}
private async _handleEnable(ev: Event) {
private async _handleEnable(
ev: HASSDomCurrentTargetEvent<HTMLElement & { entry: ConfigEntry }>
) {
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
const entryId = button.entry.entry_id;
try {
@@ -474,7 +477,9 @@ export class BluetoothAdapterInfoPage extends LitElement {
}
}
private _openOptionFlow(ev: Event) {
private _openOptionFlow(
ev: HASSDomCurrentTargetEvent<HTMLElement & { entry: ConfigEntry }>
) {
ev.preventDefault();
ev.stopPropagation();
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
@@ -95,7 +95,6 @@ export class DHCPConfigPanel extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config/integrations/integration/dhcp"
.columns=${this._columns(this.hass.localize)}
.data=${this._dataWithIds(this._data)}
.noDataText=${this.hass.localize(
@@ -3,6 +3,7 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog-footer";
@@ -211,7 +212,9 @@ class DialogMatterLockManage extends LitElement {
`;
}
private _handleUserClick(ev: Event): void {
private _handleUserClick(
ev: HASSDomCurrentTargetEvent<HTMLElement & { user: MatterLockUser }>
): void {
// Ignore clicks that originated from the delete button
const path = ev.composedPath();
if (path.some((el) => (el as HTMLElement).tagName === "HA-ICON-BUTTON")) {
@@ -221,7 +224,9 @@ class DialogMatterLockManage extends LitElement {
this._editUser(user);
}
private _handleDeleteUserClick(ev: Event): void {
private _handleDeleteUserClick(
ev: HASSDomCurrentTargetEvent<HTMLElement & { user: MatterLockUser }>
): void {
ev.preventDefault();
ev.stopPropagation();
const user = (ev.currentTarget as any).user as MatterLockUser;
@@ -61,11 +61,7 @@ export class MQTTConfigPanel extends LitElement {
protected render(): TemplateResult {
return html`
<hass-subpage
.narrow=${this.narrow}
.hass=${this.hass}
back-path="/config/integrations/integration/mqtt"
>
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
<div class="content">
<ha-card
.header=${this.hass.localize("ui.panel.config.mqtt.settings_title")}
@@ -99,7 +99,6 @@ export class SSDPConfigPanel extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config/integrations/integration/ssdp"
.columns=${this._columns(this.hass.localize)}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
@@ -106,7 +106,6 @@ export class ZeroconfConfigPanel extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config/integrations/integration/zeroconf"
.columns=${this._columns(this.hass.localize)}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
@@ -76,7 +76,6 @@ class ZHAAddDevicesPage extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/zha/dashboard"
.header=${this.hass.localize("ui.panel.config.zha.add_device")}
>
<ha-button
@@ -11,10 +11,7 @@ import { addGroup, fetchGroupableDevices } from "../../../../../data/zha";
import "../../../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../../../types";
import "./zha-device-endpoint-list";
import type {
DeviceEndpointSelectionChangedEvent,
ZHADeviceEndpointList,
} from "./zha-device-endpoint-list";
import type { ZHADeviceEndpointList } from "./zha-device-endpoint-list";
@customElement("zha-add-group-page")
export class ZHAAddGroupPage extends LitElement {
@@ -131,7 +128,7 @@ export class ZHAAddGroupPage extends LitElement {
}
private _handleAddSelectionChanged(
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
ev: HASSDomEvent<HASSDomEvents["selection-changed"]>
): void {
this._selectedDevicesToAdd = ev.detail.value;
}
@@ -4,10 +4,15 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-list";
import "../../../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../../../components/input/ha-input-search";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-list-item-option";
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
@@ -269,11 +274,16 @@ export class ZHADeviceEndpointList extends LitElement {
.join(" · ");
}
private _handleFilterChanged(ev: Event): void {
this._filter = (ev.currentTarget as HTMLInputElement).value;
private _handleFilterChanged(
ev: HASSDomCurrentTargetEvent<HaInputSearch>
): void {
this._filter = ev.currentTarget.value ?? "";
}
private _handleItemSelected(ev: CustomEvent<number>): void {
private _handleItemSelected(
ev: HASSDomEvent<HASSDomEvents["ha-list-item-selected"]> &
HASSDomCurrentTargetEvent<HaListSelectable>
): void {
const list = ev.currentTarget as HaListSelectable;
let selectedDeviceIds = this._selectedDeviceIds;
@@ -290,7 +300,10 @@ export class ZHADeviceEndpointList extends LitElement {
}
}
private _handleItemDeselected(ev: CustomEvent<number>): void {
private _handleItemDeselected(
ev: HASSDomEvent<HASSDomEvents["ha-list-item-deselected"]> &
HASSDomCurrentTargetEvent<HaListSelectable>
): void {
const list = ev.currentTarget as HaListSelectable;
let selectedDeviceIds = this._selectedDeviceIds;
@@ -19,10 +19,7 @@ import "../../../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../../../types";
import { formatAsPaddedHex } from "./functions";
import "./zha-device-endpoint-list";
import type {
DeviceEndpointSelectionChangedEvent,
ZHADeviceEndpointList,
} from "./zha-device-endpoint-list";
import type { ZHADeviceEndpointList } from "./zha-device-endpoint-list";
import { showZHAAddGroupMembersDialog } from "./show-dialog-zha-add-group-members";
@customElement("zha-group-page")
@@ -202,7 +199,7 @@ export class ZHAGroupPage extends LitElement {
}
private _handleRemoveSelectionChanged(
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
ev: HASSDomEvent<HASSDomEvents["selection-changed"]>
): void {
this._selectedDevicesToRemove = ev.detail.value;
}
@@ -55,7 +55,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/zha/dashboard"
.header=${this.hass.localize(
"ui.panel.config.zha.visualization.header"
)}
@@ -1,13 +1,16 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-select";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import type { ZHAConfiguration } from "../../../../../data/zha";
import {
fetchZHAConfiguration,
@@ -18,6 +21,8 @@ import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
const PREDEFINED_TIMEOUTS = [1800, 3600, 7200, 21600, 43200, 86400];
type ZHASwitchChangeEvent = HASSDomCurrentTargetEvent<HaSwitch>;
type ZHAInputChangeEvent = HASSDomCurrentTargetEvent<HaInput>;
@customElement("zha-options-page")
class ZHAOptionsPage extends LitElement {
@@ -345,53 +350,54 @@ class ZHAOptionsPage extends LitElement {
`;
}
private _enableIdentifyOnJoinChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
this._configuration!.data.zha_options.enable_identify_on_join = checked;
private _enableIdentifyOnJoinChanged(ev: ZHASwitchChangeEvent): void {
this._configuration!.data.zha_options.enable_identify_on_join =
ev.currentTarget.checked;
this.requestUpdate();
}
private _enhancedLightTransitionChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
this._configuration!.data.zha_options.enhanced_light_transition = checked;
private _enhancedLightTransitionChanged(ev: ZHASwitchChangeEvent): void {
this._configuration!.data.zha_options.enhanced_light_transition =
ev.currentTarget.checked;
this.requestUpdate();
}
private _lightTransitioningFlagChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
this._configuration!.data.zha_options.light_transitioning_flag = checked;
private _lightTransitioningFlagChanged(ev: ZHASwitchChangeEvent): void {
this._configuration!.data.zha_options.light_transitioning_flag =
ev.currentTarget.checked;
this.requestUpdate();
}
private _groupMembersAssumeStateChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
this._configuration!.data.zha_options.group_members_assume_state = checked;
private _groupMembersAssumeStateChanged(ev: ZHASwitchChangeEvent): void {
this._configuration!.data.zha_options.group_members_assume_state =
ev.currentTarget.checked;
this.requestUpdate();
}
private _enableMainsStartupPollingChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
private _enableMainsStartupPollingChanged(ev: ZHASwitchChangeEvent): void {
this._configuration!.data.zha_options.enable_mains_startup_polling =
checked;
ev.currentTarget.checked;
this.requestUpdate();
}
private _defaultLightTransitionChanged(ev: Event): void {
const value = Number((ev.target as HTMLInputElement).value);
this._configuration!.data.zha_options.default_light_transition = value;
private _defaultLightTransitionChanged(ev: ZHAInputChangeEvent): void {
this._configuration!.data.zha_options.default_light_transition = Number(
ev.currentTarget.value
);
this.requestUpdate();
}
private _customMainsSecondsChanged(ev: Event): void {
const seconds = Number((ev.target as HTMLInputElement).value);
this._configuration!.data.zha_options.consider_unavailable_mains = seconds;
private _customMainsSecondsChanged(ev: ZHAInputChangeEvent): void {
this._configuration!.data.zha_options.consider_unavailable_mains = Number(
ev.currentTarget.value
);
this.requestUpdate();
}
private _customBatterySecondsChanged(ev: Event): void {
const seconds = Number((ev.target as HTMLInputElement).value);
this._configuration!.data.zha_options.consider_unavailable_battery =
seconds;
private _customBatterySecondsChanged(ev: ZHAInputChangeEvent): void {
this._configuration!.data.zha_options.consider_unavailable_battery = Number(
ev.currentTarget.value
);
this.requestUpdate();
}
@@ -419,7 +425,15 @@ class ZHAOptionsPage extends LitElement {
this.requestUpdate();
}
private async _updateConfiguration(ev: Event): Promise<void> {
private async _updateConfiguration(
ev: HASSDomCurrentTargetEvent<
HTMLElement & {
progress: boolean;
actionSuccess: () => void;
actionError: () => void;
}
>
): Promise<void> {
const button = ev.currentTarget as HTMLElement & {
progress: boolean;
actionSuccess: () => void;
@@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../../common/translations/localize";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
@@ -478,7 +479,9 @@ class DialogZwaveCredentialUserEdit extends DirtyStateProviderMixin<CredentialFo
}
}
private _handleCredentialTypeChanged(ev: Event): void {
private _handleCredentialTypeChanged(
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
): void {
this._credentialType = (ev.currentTarget as HaRadioGroup)
.value as ZwaveCredentialType;
// Switching types invalidates any data tied to the previous type's
@@ -34,12 +34,7 @@ class ZWaveJSConfigEntryPicker extends LitElement {
if (this._configEntries.length === 0) {
return html`
<hass-subpage
header="Z-Wave"
.narrow=${this.narrow}
.hass=${this.hass}
back-path="/config/integrations/integration/zwave_js"
>
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
<div class="content">
<ha-card>
<div class="card-content">
@@ -56,12 +51,7 @@ class ZWaveJSConfigEntryPicker extends LitElement {
}
return html`
<hass-subpage
header="Z-Wave"
.narrow=${this.narrow}
.hass=${this.hass}
back-path="/config/integrations/integration/zwave_js"
>
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
<div class="content">
<ha-card
.header=${this.hass.localize(
@@ -2,11 +2,13 @@ import { mdiCloseCircle } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-button";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import "../../../../../components/ha-spinner";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import {
getZwaveNodeRawConfigParameter,
setZwaveNodeRawConfigParameter,
@@ -129,9 +131,9 @@ class ZWaveJSCustomParam extends LitElement {
return parsed;
}
private _customParamNumberChanged(ev: Event) {
private _customParamNumberChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
this._customParamNumber = this._tryParseNumber(
(ev.target as HTMLInputElement).value
ev.currentTarget.value ?? ""
);
}
@@ -139,8 +141,8 @@ class ZWaveJSCustomParam extends LitElement {
this._valueSize = this._tryParseNumber(ev.detail.value) ?? 1;
}
private _customValueChanged(ev: Event) {
this._value = this._tryParseNumber((ev.target as HTMLInputElement).value);
private _customValueChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
this._value = this._tryParseNumber(ev.currentTarget.value ?? "");
}
private _customValueFormatChanged(ev: HaSelectSelectEvent) {
+4 -1
View File
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-alert";
@@ -283,7 +284,9 @@ export class SystemLogCard extends LitElement {
fileDownload(signedUrl.path, logFileName);
}
private _openLog(ev: Event): void {
private _openLog(
ev: HASSDomCurrentTargetEvent<HTMLElement & { logItem: LoggedError }>
): void {
const item = (ev.currentTarget as any).logItem;
showSystemLogDetailDialog(this, { item });
}
@@ -175,7 +175,6 @@ export class HaConfigLovelaceResources extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config/lovelace/dashboards"
.tabs=${lovelaceResourcesTabs}
.columns=${this._columns(this.hass.language, this.hass.localize)}
.data=${this._resources}
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { mainWindow } from "../../../common/dom/get_main_window";
import { HOSTNAME_PATTERN } from "../../../common/string/is_hostname";
import {
IP_ADDRESS_OR_NETWORK_PATTERN,
IP_ADDRESS_PATTERN,
@@ -38,7 +39,16 @@ const SCHEMA = memoizeOne(
{
name: "server_port",
required: true,
selector: { number: { min: 1, max: 65535, mode: "box" } },
selector: {
number: {
min: 1,
max: 65535,
mode: "box",
validation_message: localize(
"ui.panel.config.network.http.invalid_port"
),
},
},
},
{
name: "ssl",
@@ -118,7 +128,16 @@ const SCHEMA = memoizeOne(
{
name: "login_attempts_threshold",
required: true,
selector: { number: { min: -1, max: 1000, mode: "box" } },
selector: {
number: {
min: -1,
max: 1000,
mode: "box",
validation_message: localize(
"ui.panel.config.network.http.invalid_login_attempts_threshold"
),
},
},
},
],
},
@@ -133,7 +152,7 @@ const SCHEMA = memoizeOne(
selector: {
text: {
multiple: true,
pattern: IP_ADDRESS_PATTERN,
pattern: `${IP_ADDRESS_PATTERN}|${HOSTNAME_PATTERN}`,
validation_message: localize(
"ui.panel.config.network.http.invalid_host"
),
@@ -2,6 +2,7 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import { isIPAddress } from "../../../common/string/is_ip_address";
import "../../../components/ha-alert";
import "../../../components/ha-button";
@@ -154,14 +155,12 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.url.description")}
</div>
<h4>
${this.hass.localize("ui.panel.config.url.external_url_label")}
</h4>
${
hasCloud
? html`
<h4>
${this.hass.localize(
"ui.panel.config.url.external_url_label"
)}
</h4>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
@@ -363,12 +362,12 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
);
}
private _toggleCloud(ev: Event) {
private _toggleCloud(ev: HASSDomCurrentTargetEvent<HaSwitch>) {
this._cloudChecked = (ev.currentTarget as HaSwitch).checked;
this._showCustomExternalUrl = !this._cloudChecked;
}
private _toggleInternalAutomatic(ev: Event) {
private _toggleInternalAutomatic(ev: HASSDomCurrentTargetEvent<HaSwitch>) {
this._showCustomInternalUrl = !(ev.currentTarget as HaSwitch).checked;
}
@@ -2,6 +2,10 @@ import { mdiDeleteOutline, mdiMenuDown, mdiPlus, mdiWifi } from "@mdi/js";
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import type {
HASSDomCurrentTargetEvent,
HASSDomTargetEvent,
} from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
@@ -630,7 +634,9 @@ export class HassioNetwork extends LitElement {
this._interface = { ...this._interfaces[this._curTabIndex] };
}
private _handleRadioValueChanged(ev: Event): void {
private _handleRadioValueChanged(
ev: HASSDomCurrentTargetEvent<HaRadioGroup & { version: "ipv4" | "ipv6" }>
): void {
const source = ev.currentTarget as HaRadioGroup;
const value = source.value as "disabled" | "auto" | "static";
const version = (source as any).version as "ipv4" | "ipv6";
@@ -645,7 +651,9 @@ export class HassioNetwork extends LitElement {
this.requestUpdate("_interface");
}
private _handleRadioValueChangedAp(ev: Event): void {
private _handleRadioValueChangedAp(
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
): void {
const source = ev.currentTarget as HaRadioGroup;
const value = source.value as "open" | "wep" | "wpa-psk";
this._wifiConfiguration!.auth = value;
@@ -653,7 +661,11 @@ export class HassioNetwork extends LitElement {
this.requestUpdate("_wifiConfiguration");
}
private _handleInputValueChanged(ev: Event): void {
private _handleInputValueChanged(
ev: HASSDomTargetEvent<
HaInput & { version: "ipv4" | "ipv6"; index: number }
>
): void {
const source = ev.target as HaInput;
const value = source.value;
const version = (ev.target as any).version as "ipv4" | "ipv6";
@@ -691,7 +703,7 @@ export class HassioNetwork extends LitElement {
}
}
private _handleInputValueChangedWifi(ev: Event): void {
private _handleInputValueChangedWifi(ev: HASSDomTargetEvent<HaInput>): void {
const source = ev.target as HaInput;
const value = source.value;
const id = source.id;
@@ -708,7 +720,9 @@ export class HassioNetwork extends LitElement {
this._wifiConfiguration![id] = value;
}
private _addAddress(ev: Event): void {
private _addAddress(
ev: HASSDomTargetEvent<HTMLElement & { version: "ipv4" | "ipv6" }>
): void {
const version = (ev.target as any).version as "ipv4" | "ipv6";
this._interface![version]!.address!.push(
version === "ipv4" ? "0.0.0.0/24" : "::/64"
@@ -717,7 +731,11 @@ export class HassioNetwork extends LitElement {
this.requestUpdate("_interface");
}
private _removeAddress(ev: Event): void {
private _removeAddress(
ev: HASSDomTargetEvent<
HTMLElement & { index: number; version: "ipv4" | "ipv6" }
>
): void {
const source = ev.target as any;
const index = source.index as number;
const version = source.version as "ipv4" | "ipv6";
@@ -752,7 +770,11 @@ export class HassioNetwork extends LitElement {
this.requestUpdate("_interface");
}
private _removeNameserver(ev: Event): void {
private _removeNameserver(
ev: HASSDomTargetEvent<
HTMLElement & { index: number; version: "ipv4" | "ipv6" }
>
): void {
const source = ev.target as any;
const index = source.index as number;
const version = source.version as "ipv4" | "ipv6";
@@ -32,6 +32,8 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
@state() private _repairsIssues: RepairsIssue[] = [];
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _showIgnored = false;
private _getFilteredIssues = memoizeOne(
@@ -75,7 +77,9 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
return html`
<hass-subpage
back-path="/config/system"
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config/system"
}
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.repairs.caption")}

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