mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-05 14:06:48 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f42a5012a9 | |||
| 90f5c7a349 | |||
| d03ba15c09 | |||
| 7143acc860 | |||
| c9c46d4507 | |||
| 5916775745 | |||
| 52ee78d8a2 | |||
| 146a089044 | |||
| 8c566b43b6 | |||
| 1089c5d1c5 | |||
| 2894113033 | |||
| b9bbef3bfc | |||
| 706382cb68 | |||
| cc17921c22 | |||
| b5ef563b71 |
@@ -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;
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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}`;
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 === ""
|
||||
|
||||
@@ -35,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>
|
||||
) => {
|
||||
@@ -237,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"
|
||||
),
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -168,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(
|
||||
@@ -388,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();
|
||||
}
|
||||
@@ -415,6 +435,9 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
}
|
||||
|
||||
if (this.configEntries && this.configEntriesInProgress) {
|
||||
this._resolveInitialRender?.();
|
||||
this._resolveInitialRender = undefined;
|
||||
|
||||
const activeElement = deepActiveElement();
|
||||
|
||||
if (
|
||||
|
||||
@@ -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(
|
||||
|
||||
+7
-2
@@ -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 };
|
||||
|
||||
+7
-2
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
@@ -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,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 });
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -33,7 +33,10 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showEditorToast } from "../automation/editor-toast";
|
||||
import "../automation/action/ha-automation-action";
|
||||
import type HaAutomationAction from "../automation/action/ha-automation-action";
|
||||
import { ManualEditorMixin } from "../automation/ha-manual-editor-mixin";
|
||||
import {
|
||||
ManualEditorMixin,
|
||||
PASTED_CONFIG_TOAST_ID,
|
||||
} from "../automation/ha-manual-editor-mixin";
|
||||
import { showPasteReplaceDialog } from "../automation/paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { manualEditorStyles, saveFabStyles } from "../automation/styles";
|
||||
import "./ha-script-fields";
|
||||
@@ -339,6 +342,7 @@ export class HaManualScriptEditor extends ManualEditorMixin<ScriptConfig>(
|
||||
|
||||
protected showPastedToastWithUndo() {
|
||||
showEditorToast(this, {
|
||||
id: PASTED_CONFIG_TOAST_ID,
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.script.editor.paste_toast_message"
|
||||
),
|
||||
|
||||
@@ -11,6 +11,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeEntityEntryName } from "../../../../common/entity/compute_entity_name";
|
||||
@@ -260,7 +261,9 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
return output;
|
||||
}
|
||||
|
||||
private _copyEntity = async (ev: Event) => {
|
||||
private _copyEntity = async (
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: HassEntity }>
|
||||
) => {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
|
||||
.entity;
|
||||
@@ -270,14 +273,18 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
});
|
||||
};
|
||||
|
||||
private _entityMoreInfo(ev: Event) {
|
||||
private _entityMoreInfo(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: HassEntity }>
|
||||
) {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
|
||||
.entity;
|
||||
fireEvent(this, "hass-more-info", { entityId: entity.entity_id });
|
||||
}
|
||||
|
||||
private _entitySelected(ev: Event) {
|
||||
private _entitySelected(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: HassEntity }>
|
||||
) {
|
||||
ev.preventDefault();
|
||||
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
|
||||
.entity;
|
||||
|
||||
@@ -16,7 +16,11 @@ import { consume, type ContextType } from "@lit/context";
|
||||
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import {
|
||||
@@ -596,7 +600,9 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
private _handleSearchChange(
|
||||
ev: InputEvent & HASSDomTargetEvent<HaInputSearch>
|
||||
) {
|
||||
if (this.filter === (ev.target as HaInputSearch).value) {
|
||||
return;
|
||||
}
|
||||
@@ -706,7 +712,9 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
private _showStatisticsAdjustSumDialog(ev: Event) {
|
||||
private _showStatisticsAdjustSumDialog(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { statistic: StatisticData }>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
showStatisticsAdjustSumDialog(this, {
|
||||
statistic: (
|
||||
@@ -782,7 +790,11 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _fixIssue = async (ev: Event) => {
|
||||
private _fixIssue = async (
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & { data: StatisticsValidationResult[] }
|
||||
>
|
||||
) => {
|
||||
const issues = (
|
||||
ev.currentTarget as HTMLElement & { data: StatisticsValidationResult[] }
|
||||
).data.sort(
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
@@ -419,7 +420,7 @@ ${
|
||||
`;
|
||||
}
|
||||
|
||||
private _splitRepositioned(ev: Event) {
|
||||
private _splitRepositioned(ev: HASSDomTargetEvent<HaSplitPanel>) {
|
||||
this._splitPosition = (ev.target as HaSplitPanel).position;
|
||||
this._storeSplitPosition();
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeEntityState } from "../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-control-slider";
|
||||
import type { ClimateEntity } from "../../../data/climate";
|
||||
import { ClimateEntityFeature } from "../../../data/climate";
|
||||
import {
|
||||
apiContext,
|
||||
formattersContext,
|
||||
@@ -31,7 +35,11 @@ import type {
|
||||
|
||||
const supportsTargetHumidityCardFeatureFromState = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "humidifier";
|
||||
return (
|
||||
domain === "humidifier" ||
|
||||
(domain === "climate" &&
|
||||
supportsFeature(stateObj, ClimateEntityFeature.TARGET_HUMIDITY))
|
||||
);
|
||||
};
|
||||
|
||||
export const supportsTargetHumidityCardFeature = (
|
||||
@@ -54,7 +62,7 @@ class HuiTargetHumidityCardFeature
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["context", "entity_id"] })
|
||||
private _stateObj?: HumidifierEntity;
|
||||
private _stateObj?: HumidifierEntity | ClimateEntity;
|
||||
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
@@ -95,7 +103,9 @@ class HuiTargetHumidityCardFeature
|
||||
}
|
||||
}
|
||||
|
||||
private _step = 1;
|
||||
private get _step() {
|
||||
return this._stateObj!.attributes.target_humidity_step ?? 1;
|
||||
}
|
||||
|
||||
private get _min() {
|
||||
return this._stateObj!.attributes.min_humidity ?? 0;
|
||||
@@ -113,7 +123,8 @@ class HuiTargetHumidityCardFeature
|
||||
}
|
||||
|
||||
private _callService() {
|
||||
this._api.callService("humidifier", "set_humidity", {
|
||||
const domain = computeStateDomain(this._stateObj!);
|
||||
this._api.callService(domain, "set_humidity", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
humidity: this._targetHumidity,
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ const DOMAIN_VARIANTS: Record<string, TileVariant[]> = {
|
||||
["climate-swing-modes"],
|
||||
["climate-swing-horizontal-modes"],
|
||||
["target-temperature"],
|
||||
["target-humidity"],
|
||||
],
|
||||
media_player: [
|
||||
TILE_VARIANT,
|
||||
|
||||
@@ -149,6 +149,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
this._yAxisFractionDigits,
|
||||
this._legendData
|
||||
)}
|
||||
.expandLegend=${this._config.expand_legend}
|
||||
click-label-for-more-info
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
|
||||
@@ -192,6 +192,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
)}
|
||||
.height=${`${Math.max(modes.includes("pie") ? 300 : 100, (this._legendData?.length || 0) * 28 + 50)}px`}
|
||||
.extraComponents=${[PieChart]}
|
||||
.expandLegend=${this._config.expand_legend}
|
||||
click-label-for-more-info
|
||||
@chart-click=${this._handleChartClick}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
|
||||
@@ -177,6 +177,7 @@ export class HuiEnergyUsageGraphCard
|
||||
this._legendData
|
||||
)}
|
||||
chart-type="bar"
|
||||
.expandLegend=${this._config.expand_legend}
|
||||
></ha-chart-base>
|
||||
${
|
||||
!this._chartData.some((dataset) => dataset.data!.length)
|
||||
|
||||
@@ -121,6 +121,7 @@ export class HuiPowerSourcesGraphCard
|
||||
this._legendData,
|
||||
this._yAxisFractionDigits
|
||||
)}
|
||||
.expandLegend=${this._config.expand_legend}
|
||||
></ha-chart-base>
|
||||
${
|
||||
!this._chartData.some((dataset) => dataset.data!.length)
|
||||
|
||||
@@ -189,6 +189,7 @@ export interface EnergyDistributionCardConfig extends EnergyCardConfig {
|
||||
export interface EnergyUsageGraphCardConfig extends EnergyCardConfig {
|
||||
type: "energy-usage-graph";
|
||||
show_legend?: boolean;
|
||||
expand_legend?: boolean;
|
||||
}
|
||||
|
||||
export interface EnergySolarGraphCardConfig extends EnergyCardConfig {
|
||||
@@ -208,11 +209,13 @@ export interface EnergyDevicesGraphCardConfig extends EnergyCardConfig {
|
||||
max_devices?: number;
|
||||
hide_compound_stats?: boolean;
|
||||
modes?: ("bar" | "pie")[];
|
||||
expand_legend?: boolean;
|
||||
}
|
||||
|
||||
export interface EnergyDevicesDetailGraphCardConfig extends EnergyCardConfig {
|
||||
type: "energy-devices-detail-graph";
|
||||
max_devices?: number;
|
||||
expand_legend?: boolean;
|
||||
}
|
||||
|
||||
export interface EnergySourcesTableCardConfig extends EnergyCardConfig {
|
||||
@@ -244,6 +247,7 @@ export interface EnergyCarbonGaugeCardConfig extends EnergyCardConfig {
|
||||
export interface PowerSourcesGraphCardConfig extends EnergyCardConfig {
|
||||
type: "power-sources-graph";
|
||||
show_legend?: boolean;
|
||||
expand_legend?: boolean;
|
||||
}
|
||||
|
||||
export interface EnergySankeyCardConfig extends EnergyCardSankeyConfig {
|
||||
|
||||
@@ -38,6 +38,7 @@ const cardConfigStruct = assign(
|
||||
max_devices: optional(number()),
|
||||
modes: optional(array(union([literal("bar"), literal("pie")]))),
|
||||
hide_compound_stats: optional(boolean()),
|
||||
expand_legend: optional(boolean()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -91,6 +92,11 @@ export class HuiEnergyDevicesCardEditor
|
||||
visible: { field: "type", value: "energy-devices-graph" },
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "expand_legend",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "max_devices",
|
||||
required: false,
|
||||
|
||||
@@ -40,6 +40,19 @@ const SCHEMA: HaFormSchema[] = [
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "expand_legend",
|
||||
visible: [
|
||||
{
|
||||
field: "type",
|
||||
operator: "in",
|
||||
value: ["power-sources-graph", "energy-usage-graph"],
|
||||
},
|
||||
{ field: "show_legend", operator: "not_eq", value: false },
|
||||
],
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "link_dashboard",
|
||||
visible: { field: "type", value: "energy-distribution" },
|
||||
@@ -73,6 +86,7 @@ const cardConfigStruct = assign(
|
||||
title: optional(string()),
|
||||
collection_key: optional(string()),
|
||||
show_legend: optional(boolean()),
|
||||
expand_legend: optional(boolean()),
|
||||
link_dashboard: optional(boolean()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -587,10 +587,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
...(sidebarSection && {
|
||||
sidebar: {
|
||||
sections: [sidebarSection],
|
||||
content_label: hass.localize("ui.panel.lovelace.strategy.home.home"),
|
||||
sidebar_label: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.summaries"
|
||||
),
|
||||
visibility: [LARGE_SCREEN_CONDITION],
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -176,6 +176,14 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
const editMode = this.lovelace.editMode;
|
||||
const hasSidebar =
|
||||
this._config?.sidebar && (this._sidebarVisible || editMode);
|
||||
// The narrow screen tab switcher is opt-in: a sidebar enables it by
|
||||
// labelling both tabs, otherwise the sidebar stacks below the content.
|
||||
const useMobileTabs = Boolean(
|
||||
this.narrow &&
|
||||
hasSidebar &&
|
||||
this._config?.sidebar?.content_label &&
|
||||
this._config?.sidebar?.sidebar_label
|
||||
);
|
||||
|
||||
const totalSectionCount =
|
||||
this._sectionColumnCount + (editMode ? 1 : 0) + (hasSidebar ? 1 : 0);
|
||||
@@ -184,7 +192,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
Math.min(this._maxColumns, totalSectionCount),
|
||||
1
|
||||
);
|
||||
// On mobile with sidebar, use full width for whichever view is active
|
||||
// On mobile with sidebar, content and sidebar both take full width
|
||||
// (stacked, or switched between via the tabs when opted in)
|
||||
const contentColumnCount =
|
||||
hasSidebar && !this.narrow ? Math.max(1, columnCount - 1) : columnCount;
|
||||
|
||||
@@ -199,6 +208,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
"top-margin": Boolean(this._config?.top_margin),
|
||||
"has-sidebar": Boolean(hasSidebar),
|
||||
narrow: this.narrow,
|
||||
"mobile-tabs-enabled": useMobileTabs,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
"--column-count": columnCount,
|
||||
@@ -213,7 +223,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.config=${this._config?.header}
|
||||
></hui-view-header>
|
||||
${
|
||||
this.narrow && hasSidebar
|
||||
useMobileTabs
|
||||
? html`
|
||||
<div class="mobile-tabs">
|
||||
<ha-control-select
|
||||
@@ -246,7 +256,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
<div
|
||||
class="content ${classMap({
|
||||
dense: Boolean(this._config?.dense_section_placement),
|
||||
"mobile-hidden": this.narrow && this._sidebarTabActive,
|
||||
"mobile-hidden": useMobileTabs && this._sidebarTabActive,
|
||||
})}"
|
||||
>
|
||||
${repeat(
|
||||
@@ -334,7 +344,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
<hui-view-sidebar
|
||||
class=${classMap({
|
||||
"mobile-hidden":
|
||||
!hasSidebar || (this.narrow && !this._sidebarTabActive),
|
||||
!hasSidebar ||
|
||||
(useMobileTabs && !this._sidebarTabActive),
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.badges=${this.badges}
|
||||
@@ -613,6 +624,9 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
.wrapper.narrow hui-view-sidebar {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.wrapper.narrow.mobile-tabs-enabled hui-view-sidebar {
|
||||
padding-bottom: calc(
|
||||
var(--ha-space-14) + var(--ha-space-3) + var(--safe-area-inset-bottom)
|
||||
);
|
||||
@@ -663,7 +677,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.wrapper.narrow.has-sidebar .content {
|
||||
.wrapper.narrow.mobile-tabs-enabled .content {
|
||||
padding-bottom: calc(
|
||||
var(--ha-space-14) + var(--ha-space-3) + var(--safe-area-inset-bottom)
|
||||
);
|
||||
|
||||
@@ -13,6 +13,9 @@ import { showToast } from "../util/toast";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
import { navigate } from "../common/navigate";
|
||||
|
||||
const CONNECTION_LOST_TOAST_ID = "connection-lost";
|
||||
const SERVER_STARTUP_TOAST_ID = "server-startup";
|
||||
|
||||
export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
class extends superClass {
|
||||
private _subscribedBootstrapIntegrations?: Promise<UnsubscribeFunc>;
|
||||
@@ -34,6 +37,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
if (oldHass?.config?.state !== this.hass!.config.state) {
|
||||
if (this.hass!.config.state === STATE_NOT_RUNNING) {
|
||||
showToast(this, {
|
||||
id: SERVER_STARTUP_TOAST_ID,
|
||||
message:
|
||||
this.hass!.localize("ui.notification_toast.starting") ||
|
||||
"Home Assistant is starting. Not everything will be available until it is finished.",
|
||||
@@ -57,6 +61,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
) {
|
||||
this._unsubscribeBootstrapIntegrations();
|
||||
showToast(this, {
|
||||
id: SERVER_STARTUP_TOAST_ID,
|
||||
message: this.hass!.localize("ui.notification_toast.started"),
|
||||
duration: 5000,
|
||||
});
|
||||
@@ -95,6 +100,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
return;
|
||||
}
|
||||
showToast(this, {
|
||||
id: CONNECTION_LOST_TOAST_ID,
|
||||
message: "",
|
||||
duration: 0,
|
||||
});
|
||||
@@ -106,6 +112,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
this._disconnectedTimeout = window.setTimeout(() => {
|
||||
this._disconnectedTimeout = undefined;
|
||||
showToast(this, {
|
||||
id: CONNECTION_LOST_TOAST_ID,
|
||||
message: this.hass!.localize("ui.notification_toast.connection_lost"),
|
||||
duration: -1,
|
||||
dismissable: false,
|
||||
@@ -120,6 +127,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
|
||||
if (Object.keys(message).length === 0) {
|
||||
showToast(this, {
|
||||
id: SERVER_STARTUP_TOAST_ID,
|
||||
message:
|
||||
this.hass!.localize("ui.notification_toast.wrapping_up_startup") ||
|
||||
`Wrapping up startup. Not everything will be available until it is finished.`,
|
||||
@@ -142,7 +150,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
)[0][0];
|
||||
|
||||
showToast(this, {
|
||||
id: "integration_starting",
|
||||
id: SERVER_STARTUP_TOAST_ID,
|
||||
message:
|
||||
this.hass!.localize("ui.notification_toast.integration_starting", {
|
||||
integration: domainToName(this.hass!.localize, integration),
|
||||
|
||||
@@ -8766,8 +8766,10 @@
|
||||
"save_error": "Could not save the HTTP configuration.",
|
||||
"port_warning": "Clients such as the Home Assistant mobile apps will lose their connection until you update the URL in their settings. If Home Assistant is not confirmed reachable on the new port, the change is rolled back automatically after 5 minutes.",
|
||||
"server_host_warning": "Leave this empty to listen on all interfaces. If you set specific addresses, Home Assistant is only reachable through those — binding it only to an address that other devices can't reach (for example localhost) will make it unreachable from those devices.",
|
||||
"invalid_host": "Enter a valid IP address.",
|
||||
"invalid_host": "Enter a valid IP address or hostname.",
|
||||
"invalid_network": "Enter a valid IP address or network.",
|
||||
"invalid_port": "Enter a port number between 1 and 65535.",
|
||||
"invalid_login_attempts_threshold": "Enter a number between -1 and 1000.",
|
||||
"running_default": "Your saved HTTP configuration could not be applied, so Home Assistant is running on the built-in default configuration.",
|
||||
"reverted_not_confirmed": "The last HTTP configuration change was not confirmed in time and was rolled back. Home Assistant is running on the previous configuration.",
|
||||
"reverted_failed": "The last HTTP configuration change could not be applied and was rolled back. Home Assistant is running on the previous configuration. Reason: {error}",
|
||||
@@ -8806,7 +8808,7 @@
|
||||
},
|
||||
"helpers": {
|
||||
"server_port": "The port Home Assistant listens on. Default is {port}.",
|
||||
"server_host": "IP addresses to bind to. Leave empty to listen on all interfaces.",
|
||||
"server_host": "IP addresses or hostnames to bind to. Leave empty to listen on all interfaces.",
|
||||
"ssl_certificate": "Absolute path to your TLS certificate (for example, /ssl/fullchain.pem).",
|
||||
"ssl_key": "Absolute path to your TLS private key (for example, /ssl/privkey.pem).",
|
||||
"ssl_peer_certificate": "Absolute path to a client certificate Home Assistant should require for secure connections.",
|
||||
@@ -8999,8 +9001,6 @@
|
||||
"others": "Others",
|
||||
"scenes": "Scenes",
|
||||
"automations": "Automations",
|
||||
"for_you": "For you",
|
||||
"home": "Home",
|
||||
"favorites": "Favorites",
|
||||
"welcome_title": "No devices here yet",
|
||||
"welcome_content": "Add lights, switches, sensors, or other smart home devices to get started.",
|
||||
@@ -10095,6 +10095,7 @@
|
||||
"double_tap_action": "Double tap behavior",
|
||||
"entities": "Entities",
|
||||
"entity": "Entity",
|
||||
"expand_legend": "Expand legend",
|
||||
"fit_mode": "Fit mode",
|
||||
"fit_mode_options": {
|
||||
"contain": "Scaled (Contain)",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { Buffer } from "node:buffer";
|
||||
import { Readable } from "node:stream";
|
||||
import { promisify } from "node:util";
|
||||
import { brotliCompress, constants } from "node:zlib";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import brotli from "../../build-scripts/brotli.mjs";
|
||||
import { file, filler, run as runStream } from "./vinyl-stub.js";
|
||||
|
||||
// What build-scripts/gulp/compress.js asks for.
|
||||
const PARAMS = {
|
||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
||||
};
|
||||
|
||||
const run = (files, options = { skipLarger: true, params: PARAMS }) =>
|
||||
runStream(brotli(options), files);
|
||||
|
||||
const compressInProcess = promisify(brotliCompress);
|
||||
|
||||
describe("brotli", () => {
|
||||
it("appends .br and matches zlib byte for byte", async () => {
|
||||
const contents = filler(4096);
|
||||
const [result] = await run([file("/out/app.js", contents)]);
|
||||
|
||||
expect(result.path).toBe("/out/app.js.br");
|
||||
expect(result.contents).toEqual(
|
||||
await compressInProcess(contents, { params: PARAMS })
|
||||
);
|
||||
});
|
||||
|
||||
it("returns contents as a Buffer, which vinyl requires", async () => {
|
||||
const [result] = await run([file("/out/app.js", filler(1024))]);
|
||||
|
||||
expect(Buffer.isBuffer(result.contents)).toBe(true);
|
||||
});
|
||||
|
||||
it("appends to the path rather than replacing the extension", async () => {
|
||||
const [result] = await run([file("/out/nested/chunk.min.js", filler(500))]);
|
||||
|
||||
expect(result.path).toBe("/out/nested/chunk.min.js.br");
|
||||
});
|
||||
|
||||
it("drops files that compression grows when skipLarger is set", async () => {
|
||||
expect(await run([file("/out/tiny.js", filler(1))])).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps files that compression grows when skipLarger is not set", async () => {
|
||||
const [result] = await run([file("/out/tiny.js", filler(1))], {
|
||||
params: PARAMS,
|
||||
});
|
||||
|
||||
expect(result.path).toBe("/out/tiny.js.br");
|
||||
});
|
||||
|
||||
it("passes null files through", async () => {
|
||||
const [result] = await run([file("/out/adirectory", null)]);
|
||||
|
||||
expect(result.path).toBe("/out/adirectory");
|
||||
expect(result.contents).toBeNull();
|
||||
});
|
||||
|
||||
it("buffers stream-mode contents", async () => {
|
||||
const contents = filler(600);
|
||||
const [result] = await run([
|
||||
file("/out/streamed.js", Readable.from([contents])),
|
||||
]);
|
||||
|
||||
expect(result.path).toBe("/out/streamed.js.br");
|
||||
expect(result.contents).toEqual(
|
||||
await compressInProcess(contents, { params: PARAMS })
|
||||
);
|
||||
});
|
||||
|
||||
it("handles more files than it keeps in flight", async () => {
|
||||
const count = 40;
|
||||
const files = Array.from({ length: count }, (_, index) =>
|
||||
file(`/out/chunk${index}.js`, filler(200 + index))
|
||||
);
|
||||
|
||||
const results = await run(files);
|
||||
|
||||
expect(results).toHaveLength(count);
|
||||
expect(new Set(results.map((result) => result.path)).size).toBe(count);
|
||||
await Promise.all(
|
||||
results.map(async (result) => {
|
||||
const index = Number(result.path.match(/chunk(\d+)/)[1]);
|
||||
expect(result.contents).toEqual(
|
||||
await compressInProcess(filler(200 + index), { params: PARAMS })
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ParallelTransform } from "../../build-scripts/parallel-transform.mjs";
|
||||
import { run } from "./vinyl-stub.js";
|
||||
|
||||
const defer = () => {
|
||||
const handle = {};
|
||||
handle.promise = new Promise((resolve, reject) => {
|
||||
handle.resolve = resolve;
|
||||
handle.reject = reject;
|
||||
});
|
||||
return handle;
|
||||
};
|
||||
|
||||
describe("ParallelTransform", () => {
|
||||
it("keeps `limit` handlers in flight and no more", async () => {
|
||||
const pending = [];
|
||||
let active = 0;
|
||||
let peak = 0;
|
||||
|
||||
const stream = new ParallelTransform(3, () => {
|
||||
active += 1;
|
||||
peak = Math.max(peak, active);
|
||||
const handle = defer();
|
||||
pending.push(handle);
|
||||
return handle.promise.then((result) => {
|
||||
active -= 1;
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
const items = Array.from({ length: 10 }, (_, index) => `item${index}`);
|
||||
const done = run(stream, items);
|
||||
|
||||
await setImmediate();
|
||||
expect(pending).toHaveLength(3);
|
||||
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
pending[index].resolve(items[index]);
|
||||
// eslint-disable-next-line no-await-in-loop -- releasing one job at a time is the point
|
||||
await setImmediate();
|
||||
}
|
||||
|
||||
expect(await done).toEqual(items);
|
||||
expect(peak).toBe(3);
|
||||
});
|
||||
|
||||
it("emits in completion order rather than input order", async () => {
|
||||
const pending = new Map();
|
||||
const stream = new ParallelTransform(3, (item) => {
|
||||
const handle = defer();
|
||||
pending.set(item, handle);
|
||||
return handle.promise;
|
||||
});
|
||||
|
||||
const done = run(stream, ["a", "b", "c"]);
|
||||
await setImmediate();
|
||||
|
||||
["c", "a", "b"].forEach((item) => pending.get(item).resolve(item));
|
||||
|
||||
expect(await done).toEqual(["c", "a", "b"]);
|
||||
});
|
||||
|
||||
it("drops results the handler resolves to nothing for", async () => {
|
||||
const stream = new ParallelTransform(2, async (item) =>
|
||||
item === "skip" ? undefined : item
|
||||
);
|
||||
|
||||
expect(await run(stream, ["a", "skip", "b"])).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("fails the stream when a handler rejects", async () => {
|
||||
const stream = new ParallelTransform(2, async (item) => {
|
||||
if (item === "bad") {
|
||||
throw new Error("boom");
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
await expect(run(stream, ["a", "bad", "b"])).rejects.toThrow("boom");
|
||||
});
|
||||
|
||||
it("does not finish until in-flight work completes", async () => {
|
||||
const handle = defer();
|
||||
let ended = false;
|
||||
|
||||
const stream = new ParallelTransform(2, () => handle.promise);
|
||||
const done = run(stream, ["a"]).then((results) => {
|
||||
ended = true;
|
||||
return results;
|
||||
});
|
||||
|
||||
await setImmediate();
|
||||
expect(ended).toBe(false);
|
||||
|
||||
handle.resolve("a");
|
||||
|
||||
expect(await done).toEqual(["a"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
// Stand-in for the vinyl files gulp.src yields in buffer mode.
|
||||
export const file = (path, contents) => ({
|
||||
path,
|
||||
contents,
|
||||
isNull() {
|
||||
return this.contents === null;
|
||||
},
|
||||
isStream() {
|
||||
return this.contents instanceof Readable;
|
||||
},
|
||||
});
|
||||
|
||||
export const filler = (length) => Buffer.alloc(length, "a");
|
||||
|
||||
export const run = (stream, files) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const out = [];
|
||||
stream.on("data", (result) => out.push(result));
|
||||
stream.on("error", reject);
|
||||
stream.on("end", () => resolve(out));
|
||||
files.forEach((entry) => stream.write(entry));
|
||||
stream.end();
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import process from "node:process";
|
||||
import { Readable } from "node:stream";
|
||||
import gfxZopfli from "@gfx/zopfli";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { file, filler, run as runStream } from "./vinyl-stub.js";
|
||||
|
||||
// Keep the pool small; it is created once, on the first factory call.
|
||||
process.env.ZOPFLI_WORKERS = "2";
|
||||
@@ -15,28 +16,8 @@ const { default: zopfli } = await import("../../build-scripts/zopfli.mjs");
|
||||
|
||||
const THRESHOLD = 150;
|
||||
|
||||
// Stand-in for the vinyl files gulp.src yields in buffer mode.
|
||||
const file = (path, contents) => ({
|
||||
path,
|
||||
contents,
|
||||
isNull() {
|
||||
return this.contents === null;
|
||||
},
|
||||
isStream() {
|
||||
return this.contents instanceof Readable;
|
||||
},
|
||||
});
|
||||
|
||||
const run = (files, options = { threshold: THRESHOLD }) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const stream = zopfli(options);
|
||||
const out = [];
|
||||
stream.on("data", (result) => out.push(result));
|
||||
stream.on("error", reject);
|
||||
stream.on("end", () => resolve(out));
|
||||
files.forEach((entry) => stream.write(entry));
|
||||
stream.end();
|
||||
});
|
||||
runStream(zopfli(options), files);
|
||||
|
||||
// What the old in-process plugin did, for byte-for-byte comparison.
|
||||
const gzipInProcess = (contents) =>
|
||||
@@ -46,8 +27,6 @@ const gzipInProcess = (contents) =>
|
||||
);
|
||||
});
|
||||
|
||||
const filler = (length) => Buffer.alloc(length, "a");
|
||||
|
||||
describe("zopfli worker pool", () => {
|
||||
it("appends .gz and matches in-process zopfli byte for byte", async () => {
|
||||
const contents = filler(4096);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { HOSTNAME_PATTERN } from "../../../src/common/string/is_hostname";
|
||||
import { IP_ADDRESS_PATTERN } from "../../../src/common/string/is_ip_address";
|
||||
|
||||
const HOSTNAMES = [
|
||||
"localhost",
|
||||
"homeassistant.lan",
|
||||
"home-assistant.local",
|
||||
"3com.com",
|
||||
"sub.domain.example.com",
|
||||
"a".repeat(63),
|
||||
];
|
||||
|
||||
const INVALID = [
|
||||
"",
|
||||
"foo bar",
|
||||
"foo_bar",
|
||||
"-foo.lan",
|
||||
"foo-.lan",
|
||||
"foo..lan",
|
||||
"héllo.lan",
|
||||
"host:8123",
|
||||
"http://host",
|
||||
"a".repeat(64),
|
||||
// All-numeric final label: a mistyped IP address, not a hostname.
|
||||
"123",
|
||||
"foo.123",
|
||||
"300.1.1.1",
|
||||
"999.999.999.999",
|
||||
];
|
||||
|
||||
// The browser anchors a `pattern` attribute as `^(?:…)$` and compiles it with
|
||||
// the `v` flag. A pattern that fails to compile under `v` is silently ignored,
|
||||
// disabling validation entirely, so compile it exactly the same way here.
|
||||
const patternRegexp = (pattern: string): RegExp =>
|
||||
new RegExp(`^(?:${pattern})$`, "v");
|
||||
|
||||
describe("HOSTNAME_PATTERN", () => {
|
||||
it("compiles as an HTML pattern attribute", () => {
|
||||
expect(() => patternRegexp(HOSTNAME_PATTERN)).not.toThrow();
|
||||
});
|
||||
it("accepts hostnames", () => {
|
||||
const regexp = patternRegexp(HOSTNAME_PATTERN);
|
||||
for (const value of HOSTNAMES) {
|
||||
expect(regexp.test(value), value).toBe(true);
|
||||
}
|
||||
});
|
||||
it("rejects malformed hostnames", () => {
|
||||
const regexp = patternRegexp(HOSTNAME_PATTERN);
|
||||
for (const value of INVALID) {
|
||||
expect(regexp.test(value), value).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// How ha-config-http-form validates its listen addresses.
|
||||
describe("IP address or hostname pattern", () => {
|
||||
const composed = `${IP_ADDRESS_PATTERN}|${HOSTNAME_PATTERN}`;
|
||||
|
||||
it("compiles as an HTML pattern attribute", () => {
|
||||
expect(() => patternRegexp(composed)).not.toThrow();
|
||||
});
|
||||
it("accepts hostnames and IP addresses", () => {
|
||||
const regexp = patternRegexp(composed);
|
||||
for (const value of [
|
||||
...HOSTNAMES,
|
||||
"192.168.1.10",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
"fe80::1",
|
||||
"::1",
|
||||
]) {
|
||||
expect(regexp.test(value), value).toBe(true);
|
||||
}
|
||||
});
|
||||
it("rejects malformed addresses", () => {
|
||||
const regexp = patternRegexp(composed);
|
||||
for (const value of INVALID) {
|
||||
expect(regexp.test(value), value).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "../../src/components/ha-control-number-buttons";
|
||||
import type { HaControlNumberButton } from "../../src/components/ha-control-number-buttons";
|
||||
|
||||
class MockResizeObserver {
|
||||
observe = vi.fn();
|
||||
|
||||
unobserve = vi.fn();
|
||||
|
||||
disconnect = vi.fn();
|
||||
}
|
||||
|
||||
describe("ha-control-number-buttons step bounds", () => {
|
||||
// An input_number with min 1, max 99 and step 10: the step grid does not
|
||||
// divide the range, so snapping used to round past both bounds.
|
||||
const RANGE = { min: 1, max: 99, step: 10 };
|
||||
|
||||
let buttons: HaControlNumberButton[] = [];
|
||||
|
||||
const mountButtons = async (
|
||||
props: Partial<HaControlNumberButton>
|
||||
): Promise<HaControlNumberButton> => {
|
||||
const el = document.createElement(
|
||||
"ha-control-number-buttons"
|
||||
) as HaControlNumberButton;
|
||||
Object.assign(el, props);
|
||||
document.body.appendChild(el);
|
||||
buttons.push(el);
|
||||
await el.updateComplete;
|
||||
return el;
|
||||
};
|
||||
|
||||
const stepButton = (el: HaControlNumberButton, label: string) =>
|
||||
el.shadowRoot!.querySelector<HTMLButtonElement>(`[aria-label="${label}"]`)!;
|
||||
|
||||
const changedValues = (el: HaControlNumberButton) => {
|
||||
const values: (number | undefined)[] = [];
|
||||
el.addEventListener("value-changed", (ev) => {
|
||||
values.push((ev as CustomEvent).detail.value);
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
const pressKey = async (el: HaControlNumberButton, code: string) => {
|
||||
el.shadowRoot!.querySelector('[role="spinbutton"]')!.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
code,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
cancelable: true,
|
||||
})
|
||||
);
|
||||
await el.updateComplete;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
buttons.forEach((el) => el.remove());
|
||||
buttons = [];
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("stops at the maximum instead of stepping past it", async () => {
|
||||
const el = await mountButtons({ ...RANGE, value: 91 });
|
||||
const values = changedValues(el);
|
||||
stepButton(el, "increment").click();
|
||||
await el.updateComplete;
|
||||
expect(values).toEqual([99]);
|
||||
expect(stepButton(el, "increment").disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("stops at the minimum instead of stepping past it", async () => {
|
||||
const el = await mountButtons({ ...RANGE, value: 11 });
|
||||
const values = changedValues(el);
|
||||
stepButton(el, "decrement").click();
|
||||
await el.updateComplete;
|
||||
expect(values).toEqual([1]);
|
||||
expect(stepButton(el, "decrement").disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("still snaps to the step grid away from the bounds", async () => {
|
||||
const el = await mountButtons({ ...RANGE, value: 44 });
|
||||
const values = changedValues(el);
|
||||
stepButton(el, "increment").click();
|
||||
stepButton(el, "decrement").click();
|
||||
await el.updateComplete;
|
||||
expect(values).toEqual([50, 40]);
|
||||
});
|
||||
|
||||
it("steps without bounds when min and max are not set", async () => {
|
||||
const el = await mountButtons({ step: 10, value: 50 });
|
||||
const values = changedValues(el);
|
||||
stepButton(el, "increment").click();
|
||||
await el.updateComplete;
|
||||
expect(values).toEqual([60]);
|
||||
});
|
||||
|
||||
it("keeps arrow keys inside the bounds", async () => {
|
||||
const el = await mountButtons({ ...RANGE, value: 91 });
|
||||
const values = changedValues(el);
|
||||
await pressKey(el, "ArrowUp");
|
||||
expect(values).toEqual([99]);
|
||||
el.value = 11;
|
||||
await pressKey(el, "ArrowDown");
|
||||
expect(values).toEqual([99, 1]);
|
||||
});
|
||||
|
||||
it("pages by ten percent of the range, but at least one step", async () => {
|
||||
const wide = await mountButtons({ min: 10, max: 20, step: 1, value: 15 });
|
||||
const wideValues = changedValues(wide);
|
||||
await pressKey(wide, "PageUp");
|
||||
expect(wideValues).toEqual([16]);
|
||||
|
||||
// A range narrower than ten steps must still page by a full step.
|
||||
const narrow = await mountButtons({ min: 0, max: 5, step: 2, value: 0 });
|
||||
const narrowValues = changedValues(narrow);
|
||||
await pressKey(narrow, "PageUp");
|
||||
expect(narrowValues).toEqual([2]);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,23 @@ const createSlider = (props: Partial<HaControlSlider>): HaControlSlider => {
|
||||
return el;
|
||||
};
|
||||
|
||||
let sliders: HaControlSlider[] = [];
|
||||
|
||||
const mountSlider = async (
|
||||
props: Partial<HaControlSlider>
|
||||
): Promise<HaControlSlider> => {
|
||||
const el = createSlider(props);
|
||||
document.body.appendChild(el);
|
||||
sliders.push(el);
|
||||
await el.updateComplete;
|
||||
return el;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
sliders.forEach((el) => el.remove());
|
||||
sliders = [];
|
||||
});
|
||||
|
||||
describe("ha-control-slider value mapping", () => {
|
||||
afterEach(() => {
|
||||
document.dir = "ltr";
|
||||
@@ -54,18 +71,6 @@ describe("ha-control-slider display rounding", () => {
|
||||
// stepped percentage such as 29 snaps to 26 * step = 28.5714…
|
||||
const FAN_STEP = 100 / 91;
|
||||
|
||||
let sliders: HaControlSlider[] = [];
|
||||
|
||||
const mountSlider = async (
|
||||
props: Partial<HaControlSlider>
|
||||
): Promise<HaControlSlider> => {
|
||||
const el = createSlider(props);
|
||||
document.body.appendChild(el);
|
||||
sliders.push(el);
|
||||
await el.updateComplete;
|
||||
return el;
|
||||
};
|
||||
|
||||
const ariaValueNow = (el: HaControlSlider) =>
|
||||
el
|
||||
.shadowRoot!.querySelector('[role="slider"]')!
|
||||
@@ -74,11 +79,6 @@ describe("ha-control-slider display rounding", () => {
|
||||
const tooltipText = (el: HaControlSlider) =>
|
||||
el.shadowRoot!.querySelector(".tooltip")!.textContent!.trim();
|
||||
|
||||
afterEach(() => {
|
||||
sliders.forEach((el) => el.remove());
|
||||
sliders = [];
|
||||
});
|
||||
|
||||
it("shows the fractional stepped value by default", async () => {
|
||||
const el = await mountSlider({ step: FAN_STEP, value: 29 });
|
||||
expect(tooltipText(el)).toBe("28.57");
|
||||
@@ -113,3 +113,83 @@ describe("ha-control-slider display rounding", () => {
|
||||
expect(ariaValueNow(el)).toBe("21.5");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ha-control-slider step bounds", () => {
|
||||
// An input_number with min 1, max 99 and step 10: the step grid does not
|
||||
// divide the range, so snapping used to round past both bounds.
|
||||
const RANGE = { min: 1, max: 99, step: 10 };
|
||||
|
||||
const pressKey = async (el: HaControlSlider, code: string) => {
|
||||
const slider = el.shadowRoot!.querySelector('[role="slider"]')!;
|
||||
const init = { code, bubbles: true, composed: true, cancelable: true };
|
||||
slider.dispatchEvent(new KeyboardEvent("keydown", init));
|
||||
slider.dispatchEvent(new KeyboardEvent("keyup", init));
|
||||
await el.updateComplete;
|
||||
};
|
||||
|
||||
const changedValues = (el: HaControlSlider) => {
|
||||
const values: (number | undefined)[] = [];
|
||||
el.addEventListener("value-changed", (ev) => {
|
||||
values.push((ev as CustomEvent).detail.value);
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
it("snaps within the bounds instead of rounding past them", () => {
|
||||
const el = createSlider(RANGE);
|
||||
expect(el.steppedValue(99)).toBe(99);
|
||||
expect(el.steppedValue(1)).toBe(1);
|
||||
// Values away from the bounds still snap to the step grid.
|
||||
expect(el.steppedValue(44)).toBe(40);
|
||||
expect(el.steppedValue(46)).toBe(50);
|
||||
});
|
||||
|
||||
it("stays inside the bounds along the whole pointer path", () => {
|
||||
// The pointer handlers compose these two, so they have to hold together.
|
||||
const el = createSlider(RANGE);
|
||||
expect(el.steppedValue(el.percentageToValue(1))).toBe(99);
|
||||
expect(el.steppedValue(el.percentageToValue(0))).toBe(1);
|
||||
});
|
||||
|
||||
it("does not overshoot the maximum with a fractional step", () => {
|
||||
// A 91-speed fan: 91 * (100 / 91) used to land on 100.00000000000001.
|
||||
const el = createSlider({ min: 0, max: 100, step: 100 / 91 });
|
||||
expect(el.steppedValue(el.percentageToValue(1))).toBe(100);
|
||||
});
|
||||
|
||||
it("shows and announces the bound, not the overshoot", async () => {
|
||||
const el = await mountSlider({
|
||||
...RANGE,
|
||||
value: 99,
|
||||
tooltipMode: "always",
|
||||
});
|
||||
expect(el.shadowRoot!.querySelector(".tooltip")!.textContent!.trim()).toBe(
|
||||
"99"
|
||||
);
|
||||
expect(
|
||||
el
|
||||
.shadowRoot!.querySelector('[role="slider"]')!
|
||||
.getAttribute("aria-valuenow")
|
||||
).toBe("99");
|
||||
});
|
||||
|
||||
it("keeps paging inside the bounds", async () => {
|
||||
const el = await mountSlider({ ...RANGE, value: 91 });
|
||||
const values = changedValues(el);
|
||||
await pressKey(el, "PageUp");
|
||||
expect(values).toEqual([99]);
|
||||
el.value = 1;
|
||||
await pressKey(el, "PageDown");
|
||||
expect(values).toEqual([99, 1]);
|
||||
});
|
||||
|
||||
it("keeps arrow keys inside the bounds", async () => {
|
||||
const el = await mountSlider({ ...RANGE, value: 91 });
|
||||
const values = changedValues(el);
|
||||
await pressKey(el, "ArrowUp");
|
||||
expect(values).toEqual([99]);
|
||||
el.value = 1;
|
||||
await pressKey(el, "ArrowDown");
|
||||
expect(values).toEqual([99, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,6 +118,19 @@ describe("ha-toast", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("shows as part of a stack without becoming a popover", async () => {
|
||||
const element = await mountToast({ stacked: true });
|
||||
|
||||
await showToast(element);
|
||||
|
||||
expect(
|
||||
element.shadowRoot!.querySelector(".toast")!.hasAttribute("popover")
|
||||
).toBe(false);
|
||||
expect(
|
||||
element.shadowRoot!.querySelector(".toast")!.classList.contains("visible")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["action", "dismiss", "programmatic"] as const)(
|
||||
"reports a %s close reason",
|
||||
async (reason) => {
|
||||
|
||||
+82
-81
@@ -4,7 +4,7 @@
|
||||
* Run with:
|
||||
* yarn test:e2e:app
|
||||
*/
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { test, expect } from "@playwright/test";
|
||||
import {
|
||||
appSidebar,
|
||||
appSidebarConfig,
|
||||
@@ -161,65 +161,100 @@ test.describe("Quick search", () => {
|
||||
|
||||
defineRouteSmokeTests(appRouteSmokeGroups);
|
||||
|
||||
const assertInitialReadiness = async (
|
||||
page: Page,
|
||||
options: {
|
||||
test("keeps the launch screen until initial panel content renders", async ({
|
||||
page,
|
||||
}) => {
|
||||
const cases: {
|
||||
name: string;
|
||||
path: string;
|
||||
loadingSelector: string;
|
||||
resolver:
|
||||
"rejectMediaBrowse" | "resolveCalendarRegistry" | "resolveMediaBrowse";
|
||||
readySelector: string;
|
||||
}
|
||||
) => {
|
||||
await goToPanel(page, options.path);
|
||||
|
||||
const launchScreen = page.locator("#ha-launch-screen");
|
||||
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
await expect(page.locator(options.loadingSelector)).toBeAttached({
|
||||
timeout: QUICK_TIMEOUT,
|
||||
});
|
||||
|
||||
await page.evaluate((resolver) => window[resolver]?.(), options.resolver);
|
||||
|
||||
await expect(page.locator(options.readySelector)).toBeAttached({
|
||||
timeout: PANEL_TIMEOUT,
|
||||
});
|
||||
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
};
|
||||
|
||||
test.describe("Initial readiness", () => {
|
||||
test("keeps the launch screen until calendar content renders", async ({
|
||||
page,
|
||||
}) => {
|
||||
await assertInitialReadiness(page, {
|
||||
resolvers: (
|
||||
| "rejectMediaBrowse"
|
||||
| "resolveCalendarRegistry"
|
||||
| "resolveConfigEntries"
|
||||
| "resolveConfigEntriesInProgress"
|
||||
| "resolveGeneratedDashboard"
|
||||
| "resolveLovelaceConfig"
|
||||
| "resolveMediaBrowse"
|
||||
)[];
|
||||
}[] = [
|
||||
{
|
||||
name: "calendar",
|
||||
path: "/?scenario=delayed-calendar#/calendar",
|
||||
loadingSelector: "ha-panel-calendar ha-spinner",
|
||||
resolver: "resolveCalendarRegistry",
|
||||
readySelector: "ha-full-calendar",
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps the launch screen until media content renders", async ({
|
||||
page,
|
||||
}) => {
|
||||
await assertInitialReadiness(page, {
|
||||
resolvers: ["resolveCalendarRegistry"],
|
||||
},
|
||||
{
|
||||
name: "media browser",
|
||||
path: "/?scenario=delayed-media-browse#/media-browser/browser",
|
||||
loadingSelector: "ha-media-player-browse > ha-spinner",
|
||||
resolver: "resolveMediaBrowse",
|
||||
readySelector: "ha-media-player-browse .no-items",
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps the launch screen until a media error renders", async ({
|
||||
page,
|
||||
}) => {
|
||||
await assertInitialReadiness(page, {
|
||||
resolvers: ["resolveMediaBrowse"],
|
||||
},
|
||||
{
|
||||
name: "integrations",
|
||||
path: "/?scenario=delayed-integrations#/config/integrations",
|
||||
loadingSelector: "ha-config-integrations-dashboard hass-loading-screen",
|
||||
readySelector: "ha-config-integrations-dashboard hass-tabs-subpage",
|
||||
resolvers: ["resolveConfigEntries", "resolveConfigEntriesInProgress"],
|
||||
},
|
||||
{
|
||||
name: "media browser error",
|
||||
path: "/?scenario=delayed-media-browse-error#/media-browser/browser",
|
||||
loadingSelector: "ha-media-player-browse > ha-spinner",
|
||||
resolver: "rejectMediaBrowse",
|
||||
readySelector: "ha-media-player-browse ha-alert",
|
||||
resolvers: ["rejectMediaBrowse"],
|
||||
},
|
||||
{
|
||||
name: "generated dashboard",
|
||||
path: "/?scenario=delayed-generated-dashboard#/climate",
|
||||
loadingSelector: "#ha-launch-screen",
|
||||
readySelector: "hui-view",
|
||||
resolvers: ["resolveGeneratedDashboard"],
|
||||
},
|
||||
{
|
||||
name: "Lovelace dashboard",
|
||||
path: "/?scenario=delayed-lovelace#/lovelace",
|
||||
loadingSelector: "#ha-launch-screen",
|
||||
readySelector: "hui-card",
|
||||
resolvers: ["resolveLovelaceConfig"],
|
||||
},
|
||||
];
|
||||
|
||||
for (const readinessCase of cases) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await test.step(readinessCase.name, async () => {
|
||||
await goToPanel(page, readinessCase.path);
|
||||
|
||||
const launchScreen = page.locator("#ha-launch-screen");
|
||||
const loadingScreen = page.locator(readinessCase.loadingSelector);
|
||||
const readyContent = page.locator(readinessCase.readySelector).first();
|
||||
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
await expect(loadingScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
await expect(readyContent).not.toBeAttached();
|
||||
|
||||
await readinessCase.resolvers.reduce(
|
||||
async (previousResolver, resolver, index) => {
|
||||
await previousResolver;
|
||||
await page.evaluate((resolverName) => {
|
||||
window[resolverName]?.();
|
||||
}, resolver);
|
||||
|
||||
if (index < readinessCase.resolvers.length - 1) {
|
||||
await expect(launchScreen).toBeAttached();
|
||||
await expect(loadingScreen).toBeAttached();
|
||||
await expect(readyContent).not.toBeAttached();
|
||||
}
|
||||
},
|
||||
Promise.resolve()
|
||||
);
|
||||
|
||||
await expect(readyContent).toBeAttached({ timeout: PANEL_TIMEOUT });
|
||||
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -227,40 +262,6 @@ test.describe("Initial readiness", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe("Lovelace dashboard", () => {
|
||||
test("keeps the launch screen until generated content renders", async ({
|
||||
page,
|
||||
}) => {
|
||||
await goToPanel(page, "/?scenario=delayed-generated-dashboard#/climate");
|
||||
|
||||
const launchScreen = page.locator("#ha-launch-screen");
|
||||
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
await expect(page.locator("hui-view")).not.toBeAttached();
|
||||
|
||||
await page.evaluate(() => window.resolveGeneratedDashboard?.());
|
||||
|
||||
await expect(page.locator("hui-view")).toBeAttached({
|
||||
timeout: PANEL_TIMEOUT,
|
||||
});
|
||||
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
});
|
||||
|
||||
test("keeps the launch screen until initial content renders", async ({
|
||||
page,
|
||||
}) => {
|
||||
await goToPanel(page, "/?scenario=delayed-lovelace#/lovelace");
|
||||
|
||||
const launchScreen = page.locator("#ha-launch-screen");
|
||||
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
await expect(page.locator("hui-card")).not.toBeAttached();
|
||||
|
||||
await page.evaluate(() => window.resolveLovelaceConfig?.());
|
||||
|
||||
await expect(page.locator("hui-card").first()).toBeAttached({
|
||||
timeout: PANEL_TIMEOUT,
|
||||
});
|
||||
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
|
||||
});
|
||||
|
||||
test("renders cards", async ({ page }) => {
|
||||
await goToPanel(page, "/lovelace");
|
||||
// At least one card should appear
|
||||
|
||||
@@ -94,6 +94,8 @@ declare global {
|
||||
__mockHass: MockHomeAssistant;
|
||||
rejectMediaBrowse?: () => void;
|
||||
resolveCalendarRegistry?: () => void;
|
||||
resolveConfigEntries?: () => void;
|
||||
resolveConfigEntriesInProgress?: () => void;
|
||||
resolveGeneratedDashboard?: () => void;
|
||||
resolveLovelaceConfig?: () => void;
|
||||
resolveMediaBrowse?: () => void;
|
||||
|
||||
@@ -185,6 +185,25 @@ const delayedCalendarScenario: Scenario = (hass) => {
|
||||
hass.mockWS("config/entity_registry/list", () => registryPromise);
|
||||
};
|
||||
|
||||
const delayedIntegrationsScenario: Scenario = (hass) => {
|
||||
addLaunchScreen();
|
||||
|
||||
hass.mockWS(
|
||||
"config_entries/subscribe",
|
||||
(_msg, _currentHass, onChange?: (updates: unknown[]) => void) => {
|
||||
window.resolveConfigEntries = () => onChange?.([]);
|
||||
return () => undefined;
|
||||
}
|
||||
);
|
||||
hass.mockWS(
|
||||
"config_entries/flow/subscribe",
|
||||
(_msg, _currentHass, onChange?: (updates: unknown[]) => void) => {
|
||||
window.resolveConfigEntriesInProgress = () => onChange?.([]);
|
||||
return () => undefined;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const delayedMediaBrowseScenario: Scenario = (hass) => {
|
||||
addLaunchScreen();
|
||||
|
||||
@@ -230,6 +249,7 @@ export const scenarios: Record<string, Scenario> = {
|
||||
"custom-theme": customThemeScenario,
|
||||
"delayed-calendar": delayedCalendarScenario,
|
||||
"delayed-generated-dashboard": delayedGeneratedDashboardScenario,
|
||||
"delayed-integrations": delayedIntegrationsScenario,
|
||||
"delayed-media-browse": delayedMediaBrowseScenario,
|
||||
"delayed-media-browse-error": delayedMediaBrowseErrorScenario,
|
||||
"light-more-info": lightMoreInfoScenario,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { validateCardConfig } from "../../gallery/src/common/validate-card-config";
|
||||
|
||||
vi.hoisted(() => {
|
||||
Object.assign(globalThis, {
|
||||
__STATIC_PATH__: "/",
|
||||
__BUILD__: "modern",
|
||||
__VERSION__: "test",
|
||||
__BACKWARDS_COMPAT__: false,
|
||||
__SUPERVISOR__: false,
|
||||
__NAMESPACE__: "frontend",
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateCardConfig", () => {
|
||||
it("accepts valid card configs", async () => {
|
||||
await expect(
|
||||
validateCardConfig({
|
||||
type: "button",
|
||||
entity: "light.bed_light",
|
||||
tap_action: {
|
||||
action: "perform-action",
|
||||
perform_action: "light.toggle",
|
||||
},
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
type: "entity-filter",
|
||||
},
|
||||
{
|
||||
type: "picture",
|
||||
},
|
||||
])("rejects invalid $type card configs", async (config) => {
|
||||
await expect(validateCardConfig(config)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -60,14 +60,6 @@ const mountManager = async () => {
|
||||
return host.shadowRoot!.querySelector("notification-manager")!;
|
||||
};
|
||||
|
||||
const deferred = () => {
|
||||
let resolve: () => void;
|
||||
const promise = new Promise<void>((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
return { promise, resolve: resolve! };
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
host?.remove();
|
||||
host = undefined;
|
||||
@@ -81,9 +73,12 @@ describe("notification-manager", () => {
|
||||
await manager.showDialog({ message: "Configuration saved" });
|
||||
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
const stack = manager.shadowRoot!.querySelector<HTMLElement>(".stack")!;
|
||||
expect(toast.labelText).toBe("Configuration saved");
|
||||
expect(toast.timeoutMs).toBe(4000);
|
||||
expect(toast.bottomOffset).toBe(0);
|
||||
expect(
|
||||
stack.style.getPropertyValue("--notification-stack-bottom-offset")
|
||||
).toBe("0px");
|
||||
expect(toast.show).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -98,21 +93,34 @@ describe("notification-manager", () => {
|
||||
});
|
||||
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
const stack = manager.shadowRoot!.querySelector<HTMLElement>(".stack")!;
|
||||
expect(toast.timeoutMs).toBe(-1);
|
||||
expect(toast.bottomOffset).toBe(16);
|
||||
expect(
|
||||
stack.style.getPropertyValue("--notification-stack-bottom-offset")
|
||||
).toBe("16px");
|
||||
|
||||
manager.shadowRoot!.querySelector<HTMLElement>("ha-icon-button")!.click();
|
||||
expect(toast.hide).toHaveBeenCalledWith("dismiss");
|
||||
});
|
||||
|
||||
it("clears the current notification when duration is zero", async () => {
|
||||
it("clears the latest anonymous notification when duration is zero without an ID", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ message: "First message", duration: -1 });
|
||||
await manager.showDialog({
|
||||
id: "identified",
|
||||
message: "Identified message",
|
||||
duration: -1,
|
||||
});
|
||||
await manager.showDialog({ message: "Second message", duration: -1 });
|
||||
|
||||
await manager.showDialog({ message: "", duration: 0 });
|
||||
await manager.updateComplete;
|
||||
|
||||
expect(manager.shadowRoot!.querySelector("ha-toast")).toBeNull();
|
||||
expect(
|
||||
[...manager.shadowRoot!.querySelectorAll("ha-toast")].map(
|
||||
(toast) => toast.labelText
|
||||
)
|
||||
).toEqual(["First message", "Identified message"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -190,6 +198,32 @@ describe("notification-manager", () => {
|
||||
expect(primary).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("invokes the action belonging to the selected stacked toast", async () => {
|
||||
const manager = await mountManager();
|
||||
const firstAction = vi.fn();
|
||||
const secondAction = vi.fn();
|
||||
await manager.showDialog({
|
||||
id: "first",
|
||||
message: "First",
|
||||
action: { action: firstAction, text: "First action" },
|
||||
duration: -1,
|
||||
});
|
||||
await manager.showDialog({
|
||||
id: "second",
|
||||
message: "Second",
|
||||
action: { action: secondAction, text: "Second action" },
|
||||
duration: -1,
|
||||
});
|
||||
|
||||
manager.shadowRoot!.querySelectorAll("ha-button")[1].click();
|
||||
|
||||
expect(secondAction).toHaveBeenCalledOnce();
|
||||
expect(firstAction).not.toHaveBeenCalled();
|
||||
expect(
|
||||
manager.shadowRoot!.querySelectorAll("ha-toast")[1].hide
|
||||
).toHaveBeenCalledWith("action");
|
||||
});
|
||||
|
||||
it("keeps a same-ID toast visible while replacing its content", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ id: "status", message: "First" });
|
||||
@@ -202,31 +236,114 @@ describe("notification-manager", () => {
|
||||
expect(toast.labelText).toBe("Second");
|
||||
});
|
||||
|
||||
it("hides a toast before replacing it with a different ID", async () => {
|
||||
it("stacks toasts with different IDs", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ id: "first", message: "First" });
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
vi.mocked(toast.hide).mockClear();
|
||||
|
||||
await manager.showDialog({ id: "second", message: "Second" });
|
||||
|
||||
expect(toast.hide).toHaveBeenCalledOnce();
|
||||
expect(toast.labelText).toBe("Second");
|
||||
const toasts = manager.shadowRoot!.querySelectorAll("ha-toast");
|
||||
expect(toasts).toHaveLength(2);
|
||||
expect([...toasts].map((toast) => toast.labelText)).toEqual([
|
||||
"First",
|
||||
"Second",
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores a stale replacement after a newer notification starts", async () => {
|
||||
it("uses the largest bottom offset for the notification stack", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({
|
||||
id: "first",
|
||||
message: "First",
|
||||
bottomOffset: 16,
|
||||
});
|
||||
await manager.showDialog({
|
||||
id: "second",
|
||||
message: "Second",
|
||||
bottomOffset: 48,
|
||||
});
|
||||
|
||||
expect(
|
||||
manager
|
||||
.shadowRoot!.querySelector<HTMLElement>(".stack")!
|
||||
.style.getPropertyValue("--notification-stack-bottom-offset")
|
||||
).toBe("48px");
|
||||
});
|
||||
|
||||
it("closes only the toast matching a duration-zero ID", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ id: "first", message: "First" });
|
||||
await manager.showDialog({ id: "second", message: "Second" });
|
||||
|
||||
await manager.showDialog({ id: "first", message: "", duration: 0 });
|
||||
await manager.updateComplete;
|
||||
|
||||
const toasts = manager.shadowRoot!.querySelectorAll("ha-toast");
|
||||
expect(toasts).toHaveLength(1);
|
||||
expect(toasts[0].labelText).toBe("Second");
|
||||
});
|
||||
|
||||
it("keeps a same-ID update that arrives while the previous toast closes", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({ id: "status", message: "First" });
|
||||
const toast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
const delayedHide = deferred();
|
||||
vi.mocked(toast.hide).mockReturnValueOnce(delayedHide.promise);
|
||||
let finishHide: () => void;
|
||||
vi.mocked(toast.hide).mockImplementation(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
finishHide = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
const staleShow = manager.showDialog({ id: "second", message: "Second" });
|
||||
await manager.showDialog({ id: "third", message: "Third" });
|
||||
delayedHide.resolve();
|
||||
await staleShow;
|
||||
const closing = manager.showDialog({
|
||||
id: "status",
|
||||
message: "",
|
||||
duration: 0,
|
||||
});
|
||||
await Promise.resolve();
|
||||
await manager.showDialog({ id: "status", message: "Second" });
|
||||
finishHide!();
|
||||
await closing;
|
||||
await manager.updateComplete;
|
||||
|
||||
expect(toast.labelText).toBe("Third");
|
||||
const remainingToast = manager.shadowRoot!.querySelector("ha-toast")!;
|
||||
expect(remainingToast.labelText).toBe("Second");
|
||||
});
|
||||
|
||||
it("keeps a frontend update visible throughout server startup", async () => {
|
||||
const manager = await mountManager();
|
||||
await manager.showDialog({
|
||||
id: "frontend-update-available",
|
||||
message: "Frontend update available",
|
||||
duration: -1,
|
||||
});
|
||||
await manager.showDialog({
|
||||
id: "server-startup",
|
||||
message: "Home Assistant is starting",
|
||||
duration: -1,
|
||||
});
|
||||
await manager.showDialog({
|
||||
id: "server-startup",
|
||||
message: "Starting recorder",
|
||||
duration: -1,
|
||||
});
|
||||
|
||||
expect(
|
||||
[...manager.shadowRoot!.querySelectorAll("ha-toast")].map(
|
||||
(toast) => toast.labelText
|
||||
)
|
||||
).toEqual(["Frontend update available", "Starting recorder"]);
|
||||
|
||||
await manager.showDialog({
|
||||
id: "server-startup",
|
||||
message: "Home Assistant started",
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
expect(
|
||||
[...manager.shadowRoot!.querySelectorAll("ha-toast")].map(
|
||||
(toast) => toast.labelText
|
||||
)
|
||||
).toEqual(["Frontend update available", "Home Assistant started"]);
|
||||
});
|
||||
|
||||
it("clears rendered state after the toast closes", async () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import "../../../../src/panels/config/script/manual-script-editor";
|
||||
import type { HaManualScriptEditor } from "../../../../src/panels/config/script/manual-script-editor";
|
||||
import { createMockHass } from "../../../fixtures/hass";
|
||||
import { showPasteReplaceDialog } from "../../../../src/panels/config/automation/paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { PASTED_CONFIG_TOAST_ID } from "../../../../src/panels/config/automation/ha-manual-editor-mixin";
|
||||
|
||||
const pasteCases = [
|
||||
{
|
||||
@@ -163,6 +164,8 @@ describe("manual automation paste", () => {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
const notification = vi.fn();
|
||||
el.addEventListener("hass-notification", notification);
|
||||
|
||||
// Call the protected method through `any` to avoid full DOM lifecycle.
|
||||
await (el as any).handlePaste(makePasteEvent(paste));
|
||||
@@ -171,6 +174,9 @@ describe("manual automation paste", () => {
|
||||
|
||||
const ev = await valueChanged;
|
||||
expect(ev.detail.value).toEqual(expected);
|
||||
expect(notification.mock.calls[0][0].detail.id).toBe(
|
||||
PASTED_CONFIG_TOAST_ID
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { supportsTargetHumidityCardFeature } from "../../../../src/panels/lovelace/card-features/hui-target-humidity-card-feature";
|
||||
import { ClimateEntityFeature } from "../../../../src/data/climate";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
const entity = (
|
||||
entityId: string,
|
||||
attributes: HassEntity["attributes"] = {}
|
||||
): HassEntity =>
|
||||
({
|
||||
entity_id: entityId,
|
||||
state: "on",
|
||||
attributes,
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
}) as HassEntity;
|
||||
|
||||
const hassWith = (...entities: HassEntity[]): HomeAssistant =>
|
||||
({
|
||||
states: Object.fromEntries(entities.map((e) => [e.entity_id, e])),
|
||||
}) as unknown as HomeAssistant;
|
||||
|
||||
describe("supportsTargetHumidityCardFeature", () => {
|
||||
it("supports a humidifier entity", () => {
|
||||
const stateObj = entity("humidifier.test");
|
||||
const hass = hassWith(stateObj);
|
||||
expect(
|
||||
supportsTargetHumidityCardFeature(hass, {
|
||||
entity_id: stateObj.entity_id,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("supports a climate entity with the TARGET_HUMIDITY feature flag", () => {
|
||||
const stateObj = entity("climate.test", {
|
||||
supported_features: ClimateEntityFeature.TARGET_HUMIDITY,
|
||||
});
|
||||
const hass = hassWith(stateObj);
|
||||
expect(
|
||||
supportsTargetHumidityCardFeature(hass, {
|
||||
entity_id: stateObj.entity_id,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not support a climate entity without the TARGET_HUMIDITY feature flag", () => {
|
||||
const stateObj = entity("climate.test", { supported_features: 0 });
|
||||
const hass = hassWith(stateObj);
|
||||
expect(
|
||||
supportsTargetHumidityCardFeature(hass, {
|
||||
entity_id: stateObj.entity_id,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not support other domains", () => {
|
||||
const stateObj = entity("light.test");
|
||||
const hass = hassWith(stateObj);
|
||||
expect(
|
||||
supportsTargetHumidityCardFeature(hass, {
|
||||
entity_id: stateObj.entity_id,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not support a context with no entity_id", () => {
|
||||
const hass = hassWith();
|
||||
expect(supportsTargetHumidityCardFeature(hass, {})).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -4032,15 +4032,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/auth-oauth-device@npm:8.0.3":
|
||||
version: 8.0.3
|
||||
resolution: "@octokit/auth-oauth-device@npm:8.0.3"
|
||||
"@octokit/auth-oauth-device@npm:8.0.4":
|
||||
version: 8.0.4
|
||||
resolution: "@octokit/auth-oauth-device@npm:8.0.4"
|
||||
dependencies:
|
||||
"@octokit/oauth-methods": "npm:^6.0.2"
|
||||
"@octokit/request": "npm:^10.0.6"
|
||||
"@octokit/types": "npm:^16.0.0"
|
||||
"@octokit/oauth-methods": "npm:^6.0.3"
|
||||
"@octokit/request": "npm:^10.0.13"
|
||||
"@octokit/types": "npm:^17.0.0"
|
||||
universal-user-agent: "npm:^7.0.0"
|
||||
checksum: 10/33b9df5ebe971226b44bfe876ba5c188c687804b6efbdca980c2e78106fbd713439d73edc451331c7ea0095b09149a8f47e0b727e6c90d78b94dc7e4caf1aa3c
|
||||
checksum: 10/1ea5a7844f98d3f92a303e84984e5fa568253ccb1acfe1ca76d256a0e94d220978c8645d2e5206e1bd55c318511dec6aac864b4156751113beeb24c435eb935a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4094,15 +4094,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/oauth-methods@npm:^6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "@octokit/oauth-methods@npm:6.0.2"
|
||||
"@octokit/oauth-methods@npm:^6.0.3":
|
||||
version: 6.0.3
|
||||
resolution: "@octokit/oauth-methods@npm:6.0.3"
|
||||
dependencies:
|
||||
"@octokit/oauth-authorization-url": "npm:^8.0.0"
|
||||
"@octokit/request": "npm:^10.0.6"
|
||||
"@octokit/request-error": "npm:^7.0.2"
|
||||
"@octokit/types": "npm:^16.0.0"
|
||||
checksum: 10/b5a9f9d361f797595e188d6e8c0286f82030df9cf6bf81023248034b85315a1180d0710aed3c255c5399f418d271a4489b169becba4b2294525a689cade85872
|
||||
"@octokit/request": "npm:^10.0.13"
|
||||
"@octokit/request-error": "npm:^7.1.1"
|
||||
"@octokit/types": "npm:^17.0.0"
|
||||
checksum: 10/cfa36ce9c2771e1791211ff48c80c469eef618b5be4c3ac74b51692fcf1189637da68b3849ed5d2e2193bdcea30b7df4f3de56b7906a31cf3b5ccbab500f93db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4113,6 +4113,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/openapi-types@npm:^28.0.0":
|
||||
version: 28.0.0
|
||||
resolution: "@octokit/openapi-types@npm:28.0.0"
|
||||
checksum: 10/031cebd35b7a110771ab9909d4d2f8ac2dba7185b55ea2ba91dfa0736f397797c768b7c12b12290e9cb42f1dd1d157ffe829d2009be8542e227780e588a40c68
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/plugin-paginate-rest@npm:^14.0.0":
|
||||
version: 14.0.0
|
||||
resolution: "@octokit/plugin-paginate-rest@npm:14.0.0"
|
||||
@@ -4144,39 +4151,39 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/plugin-retry@npm:8.1.0":
|
||||
version: 8.1.0
|
||||
resolution: "@octokit/plugin-retry@npm:8.1.0"
|
||||
"@octokit/plugin-retry@npm:8.1.1":
|
||||
version: 8.1.1
|
||||
resolution: "@octokit/plugin-retry@npm:8.1.1"
|
||||
dependencies:
|
||||
"@octokit/request-error": "npm:^7.0.2"
|
||||
"@octokit/types": "npm:^16.0.0"
|
||||
"@octokit/request-error": "npm:^7.1.1"
|
||||
"@octokit/types": "npm:^17.0.0"
|
||||
bottleneck: "npm:^2.15.3"
|
||||
peerDependencies:
|
||||
"@octokit/core": ">=7"
|
||||
checksum: 10/0bccaa14ef295ac5dc3e6fa96bb7c555b8b188dfe0bf1db5ea83acb29af375bf08a43e0d44c42941608afc6ab414b6dcdfb44881a8e8346b963d7501e0aea766
|
||||
checksum: 10/ee42b70c04b6749362093bf79d3b5f4019a3a5e3cdf79e28e9d781e4a43242aa67781d1e71dba069646ea6a0b442e155403877493cd3e194e7045386b26023d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/request-error@npm:^7.0.2":
|
||||
version: 7.1.0
|
||||
resolution: "@octokit/request-error@npm:7.1.0"
|
||||
"@octokit/request-error@npm:^7.0.2, @octokit/request-error@npm:^7.1.1":
|
||||
version: 7.1.1
|
||||
resolution: "@octokit/request-error@npm:7.1.1"
|
||||
dependencies:
|
||||
"@octokit/types": "npm:^16.0.0"
|
||||
checksum: 10/c1d447ff7482382c69f7a4b2eaa44c672906dd111d8a9196a5d07f2adc4ae0f0e12ec4ce0063f14f9b2fb5f0cef4451c95ec961a7a711bd900e5d6441d546570
|
||||
"@octokit/types": "npm:^17.0.0"
|
||||
checksum: 10/78f91331f523e004e6e1b505bfe907aede05cd8da25abb16c62df40c31e4dceed5d979a6f30de0c088f7b03e7676495af5e282c54320ad8805864e6ee82d02f3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/request@npm:^10.0.6":
|
||||
version: 10.0.11
|
||||
resolution: "@octokit/request@npm:10.0.11"
|
||||
"@octokit/request@npm:^10.0.13, @octokit/request@npm:^10.0.6":
|
||||
version: 10.0.13
|
||||
resolution: "@octokit/request@npm:10.0.13"
|
||||
dependencies:
|
||||
"@octokit/endpoint": "npm:^11.0.3"
|
||||
"@octokit/request-error": "npm:^7.0.2"
|
||||
"@octokit/types": "npm:^16.0.0"
|
||||
"@octokit/request-error": "npm:^7.1.1"
|
||||
"@octokit/types": "npm:^17.0.0"
|
||||
content-type: "npm:^2.0.0"
|
||||
json-with-bigint: "npm:^3.5.3"
|
||||
universal-user-agent: "npm:^7.0.2"
|
||||
checksum: 10/036640f49e4ab63470130dccafb828822a18cf9ce060b8170e56ba9ce7173029db92b3c37ad7474acf2c097180d5127754b69d3ffbe60162489aed4324b3cb8f
|
||||
checksum: 10/6edce2ab7c5cbb29c65374929f5342c00445a6b0bb4788b2da23bbfe6844456949f5fcc3f1b17b955902215723836b72fa474e31d051df98f4f90dfb7c62ebe5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4201,6 +4208,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/types@npm:^17.0.0":
|
||||
version: 17.0.0
|
||||
resolution: "@octokit/types@npm:17.0.0"
|
||||
dependencies:
|
||||
"@octokit/openapi-types": "npm:^28.0.0"
|
||||
checksum: 10/38db90d8a2dde153b84a26b6ba2aa4e4a8ddda03dd13d6b4a6f075ffe850c946b6852ba77457e4de8c07b10d06f59d174223e69243c88ecb5cf6280ee541601e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@oxc-project/types@npm:=0.139.0":
|
||||
version: 0.139.0
|
||||
resolution: "@oxc-project/types@npm:0.139.0"
|
||||
@@ -9714,16 +9730,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gulp-brotli@npm:3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "gulp-brotli@npm:3.0.0"
|
||||
dependencies:
|
||||
plugin-error: "npm:^1.0.1"
|
||||
through2: "npm:^3.0.1"
|
||||
checksum: 10/0eea1fc60ae7f256184155b61a30a916007d21d37234698d8cdb299f64f71b4d68ca3182528e7da5d71290079c32c0228573578b76f5af7af7230c31537ef9d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gulp-cli@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "gulp-cli@npm:3.1.0"
|
||||
@@ -9915,8 +9921,8 @@ __metadata:
|
||||
"@material/web": "npm:2.5.0"
|
||||
"@mdi/js": "npm:7.4.47"
|
||||
"@mdi/svg": "npm:7.4.47"
|
||||
"@octokit/auth-oauth-device": "npm:8.0.3"
|
||||
"@octokit/plugin-retry": "npm:8.1.0"
|
||||
"@octokit/auth-oauth-device": "npm:8.0.4"
|
||||
"@octokit/plugin-retry": "npm:8.1.1"
|
||||
"@octokit/rest": "npm:22.0.1"
|
||||
"@playwright/test": "npm:1.62.1"
|
||||
"@replit/codemirror-indentation-markers": "npm:6.5.3"
|
||||
@@ -9979,7 +9985,6 @@ __metadata:
|
||||
glob: "npm:13.0.6"
|
||||
globals: "npm:17.8.0"
|
||||
gulp: "npm:5.0.1"
|
||||
gulp-brotli: "npm:3.0.0"
|
||||
gulp-json-transform: "npm:0.5.0"
|
||||
gulp-rename: "npm:2.1.0"
|
||||
hls.js: "npm:1.6.16"
|
||||
@@ -12977,17 +12982,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:2 || 3, readable-stream@npm:^3.4.0":
|
||||
version: 3.6.2
|
||||
resolution: "readable-stream@npm:3.6.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.3"
|
||||
string_decoder: "npm:^1.1.1"
|
||||
util-deprecate: "npm:^1.0.1"
|
||||
checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6":
|
||||
version: 2.3.8
|
||||
resolution: "readable-stream@npm:2.3.8"
|
||||
@@ -13003,6 +12997,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^3.4.0":
|
||||
version: 3.6.2
|
||||
resolution: "readable-stream@npm:3.6.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.3"
|
||||
string_decoder: "npm:^1.1.1"
|
||||
util-deprecate: "npm:^1.0.1"
|
||||
checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^4.7.0":
|
||||
version: 4.7.0
|
||||
resolution: "readable-stream@npm:4.7.0"
|
||||
@@ -14561,16 +14566,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"through2@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "through2@npm:3.0.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.4"
|
||||
readable-stream: "npm:2 || 3"
|
||||
checksum: 10/98bdffba8e877fd8beb2154adc4eb0d52fad281130f56f6e5d18f85d1e1aa528a7b27317b302eb5443f6636ab045d3c272e6dffc61d984775db284823b90532d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"time-stamp@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "time-stamp@npm:1.1.0"
|
||||
|
||||
Reference in New Issue
Block a user