mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-05 22:16:31 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25b3d20321 | |||
| 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.
|
||||
|
||||
+1
-31
@@ -17,9 +17,6 @@ const rspackConfigPath = fileURLToPath(
|
||||
new URL("./rspack.config.cjs", import.meta.url)
|
||||
);
|
||||
|
||||
// Applies everywhere, including the files exempted from the history rule below.
|
||||
const restrictedSyntax = ["LabeledStatement", "WithStatement"];
|
||||
|
||||
export default tseslint.config(
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
@@ -114,16 +111,7 @@ export default tseslint.config(
|
||||
"no-bitwise": "error",
|
||||
"no-console": "error",
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
...restrictedSyntax,
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.property.name=/^(push|replace)State$/]",
|
||||
message:
|
||||
"Use navigate(), updateHistoryState() or replaceCurrentUrl() from common/navigate. History entries carry the app's own bookkeeping, which a raw pushState/replaceState drops.",
|
||||
},
|
||||
],
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"wc/no-self-class": "off",
|
||||
|
||||
// import-x rules
|
||||
@@ -234,24 +222,6 @@ export default tseslint.config(
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// These own history entries themselves: the navigation helpers, the dialog
|
||||
// stack, the boot paths that run before the app has any state to keep, and
|
||||
// the tests that fabricate entries to simulate a document load.
|
||||
files: [
|
||||
"src/common/navigate.ts",
|
||||
"src/dialogs/make-dialog-manager.ts",
|
||||
"src/state/url-sync-mixin.ts",
|
||||
"src/panels/config/automation/add-automation-element-dialog.ts",
|
||||
"src/entrypoints/core.ts",
|
||||
"src/onboarding/**/*.ts",
|
||||
"cast/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
],
|
||||
rules: {
|
||||
"no-restricted-syntax": ["error", ...restrictedSyntax],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
|
||||
import { getCardElementClass } from "../../../src/panels/lovelace/create-element/create-card-element";
|
||||
|
||||
export const validateCardConfig = async (config: LovelaceCardConfig) => {
|
||||
const cardClass = await getCardElementClass(config.type);
|
||||
new cardClass().setConfig(config);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ReactiveElement } from "lit";
|
||||
import { getHistoryState, updateHistoryState } from "../navigate";
|
||||
import { throttle } from "../util/throttle";
|
||||
|
||||
const throttleReplaceState = throttle((value) => {
|
||||
updateHistoryState({ scrollPosition: value });
|
||||
history.replaceState({ scrollPosition: value }, "");
|
||||
}, 300);
|
||||
|
||||
export function restoreScroll(selector: string) {
|
||||
@@ -40,8 +39,7 @@ export function restoreScroll(selector: string) {
|
||||
newDescriptor = {
|
||||
get(this: ReactiveElement) {
|
||||
return (
|
||||
this[`__${String(propertyKey)}`] ||
|
||||
getHistoryState()?.scrollPosition
|
||||
this[`__${String(propertyKey)}`] || history.state?.scrollPosition
|
||||
);
|
||||
},
|
||||
set(this: ReactiveElement, value) {
|
||||
|
||||
+39
-77
@@ -1,7 +1,6 @@
|
||||
import { closeAllDialogs } from "../dialogs/make-dialog-manager";
|
||||
import { fireEvent } from "./dom/fire_event";
|
||||
import { mainWindow } from "./dom/get_main_window";
|
||||
import { currentPath } from "./url/current-path";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -18,32 +17,6 @@ export interface NavigateOptions {
|
||||
// max time to wait for dialogs to close before navigating
|
||||
const DIALOG_WAIT_TIMEOUT = 500;
|
||||
|
||||
/**
|
||||
* State of the current history entry. Always read through this, the app writes
|
||||
* to the main window and a panel running in an iframe has its own history.
|
||||
*/
|
||||
export const getHistoryState = (): any => mainWindow.history.state;
|
||||
|
||||
/**
|
||||
* Merge into the current history entry's state, keeping what is already there.
|
||||
* Entries carry the app's own bookkeeping (`from`, `root`, dialog state), so
|
||||
* they must never be replaced wholesale.
|
||||
*/
|
||||
export const updateHistoryState = (patch: Record<string, unknown>) => {
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, ...patch },
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrite the URL of the current history entry without navigating and without
|
||||
* touching its state. For query parameter cleanup.
|
||||
*/
|
||||
export const replaceCurrentUrl = (url: string) => {
|
||||
mainWindow.history.replaceState(mainWindow.history.state, "", url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stash a destination URL in the current history entry's state. If the page
|
||||
* is refreshed while a dialog is open, urlSyncMixin will navigate to this URL
|
||||
@@ -51,7 +24,10 @@ export const replaceCurrentUrl = (url: string) => {
|
||||
* The current URL is not changed.
|
||||
*/
|
||||
export const setRefreshUrl = (path: string) => {
|
||||
updateHistoryState({ refreshUrl: path });
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, refreshUrl: path },
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -87,44 +63,37 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
}
|
||||
const replace = options?.replace || false;
|
||||
|
||||
if (__DEMO__ && !path.includes("#")) {
|
||||
// The demo routes with the hash instead of the pathname. Resolve the
|
||||
// path like the browser would do for pushState, and keep the query
|
||||
// parameters in the URL query instead of inside the hash.
|
||||
const url = new URL(
|
||||
path,
|
||||
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
|
||||
);
|
||||
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
|
||||
}
|
||||
|
||||
const { history } = mainWindow;
|
||||
|
||||
if (replace) {
|
||||
// A replaced entry keeps its predecessor, so it keeps `from`.
|
||||
const { root, from } = history.state ?? {};
|
||||
const state = root ? { root: true } : (options?.data ?? null);
|
||||
history.replaceState(
|
||||
from === undefined ? state : { ...state, from },
|
||||
if (__DEMO__) {
|
||||
if (!path.includes("#")) {
|
||||
// The demo routes with the hash instead of the pathname. Resolve the
|
||||
// path like the browser would do for pushState, and keep the query
|
||||
// parameters in the URL query instead of inside the hash.
|
||||
const url = new URL(
|
||||
path,
|
||||
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
|
||||
);
|
||||
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
|
||||
}
|
||||
if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root
|
||||
? { root: true }
|
||||
: (options?.data ?? null),
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
mainWindow.history.pushState(options?.data ?? null, "", path);
|
||||
}
|
||||
} else if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
const { data } = options ?? {};
|
||||
// Only objects survive being merged with `from`. Callers outside the app
|
||||
// pass whatever they want, so keep anything else under a key rather than
|
||||
// letting the spread mangle it.
|
||||
const mergeable =
|
||||
data === undefined || (typeof data === "object" && data !== null);
|
||||
history.pushState(
|
||||
mergeable
|
||||
? { ...data, from: currentPath() }
|
||||
: { data, from: currentPath() },
|
||||
"",
|
||||
path
|
||||
);
|
||||
mainWindow.history.pushState(options?.data ?? null, "", path);
|
||||
}
|
||||
|
||||
fireEvent(mainWindow, "location-changed", {
|
||||
replace,
|
||||
});
|
||||
@@ -132,17 +101,8 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the previous history entry is a page this app navigated away from.
|
||||
* `history.length` cannot answer this: a login redirect goes through
|
||||
* `location.assign`, which leaves /auth/authorize right behind the requested
|
||||
* page, and going back there would bounce the user out of the app.
|
||||
*/
|
||||
export const canGoBack = (): boolean =>
|
||||
mainWindow.history.state?.from !== undefined;
|
||||
|
||||
/**
|
||||
* Navigate back to the page we came from, falling back to a path when the
|
||||
* previous entry is not ours (deep link, login redirect, fresh tab).
|
||||
* Navigate back in history, with fallback to a default path if no history exists.
|
||||
* This prevents a user from getting stuck when they navigate directly to a page with no history.
|
||||
*/
|
||||
export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||
const canProceed = await ensureDialogsClosed(Date.now());
|
||||
@@ -150,12 +110,14 @@ export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read after closing dialogs: their history entries are popped by then, so
|
||||
// this is the state of the page entry.
|
||||
if (canGoBack()) {
|
||||
mainWindow.history.back();
|
||||
// Check if we have history to go back to
|
||||
const { history } = mainWindow;
|
||||
if (history.length > 1) {
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
await navigate(fallbackPath || "/", { replace: true });
|
||||
// No history available, navigate to fallback path
|
||||
const fallback = fallbackPath || "/";
|
||||
navigate(fallback, { replace: true });
|
||||
};
|
||||
|
||||
@@ -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}`;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { mainWindow } from "../dom/get_main_window";
|
||||
|
||||
/**
|
||||
* The path of the page currently shown by the app. The demo routes with the
|
||||
* hash instead of the pathname, see navigate().
|
||||
*/
|
||||
export const currentPath = (): string =>
|
||||
__DEMO__
|
||||
? mainWindow.location.hash.substring(1)
|
||||
: mainWindow.location.pathname;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,11 +40,7 @@ import {
|
||||
getEntityEntryContext,
|
||||
} from "../../common/entity/context/get_entity_context";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import {
|
||||
getHistoryState,
|
||||
navigate,
|
||||
updateHistoryState,
|
||||
} from "../../common/navigate";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { withViewTransition } from "../../common/util/view-transition";
|
||||
@@ -272,12 +268,16 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
}
|
||||
|
||||
private _setView(view: MoreInfoView) {
|
||||
updateHistoryState({
|
||||
dialogParams: {
|
||||
...getHistoryState()?.dialogParams,
|
||||
view,
|
||||
history.replaceState(
|
||||
{
|
||||
...history.state,
|
||||
dialogParams: {
|
||||
...history.state?.dialogParams,
|
||||
view,
|
||||
},
|
||||
},
|
||||
});
|
||||
""
|
||||
);
|
||||
this._currView = view;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { goBack } from "../common/navigate";
|
||||
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
|
||||
|
||||
/**
|
||||
* Shared behavior of the toolbar back arrow. The arrow is a link to the
|
||||
* declared parent page so it can be opened in a new tab, but a plain click
|
||||
* returns to the page the user came from instead.
|
||||
*/
|
||||
export const handleBackClick = (
|
||||
ev: MouseEvent,
|
||||
backPath?: string,
|
||||
backCallback?: () => void
|
||||
): void => {
|
||||
const path = sanitizeNavigationPath(backPath);
|
||||
|
||||
// Middle click, ctrl and cmd open the parent in a new tab, let the anchor
|
||||
// handle those.
|
||||
if (path && !isNavigationClick(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (backCallback) {
|
||||
backCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
goBack(path);
|
||||
};
|
||||
+19
-10
@@ -4,8 +4,8 @@ import { customElement, eventOptions, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import { goBack } from "../common/navigate";
|
||||
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
|
||||
import { handleBackClick } from "./back-navigation";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
@@ -37,14 +37,19 @@ class HassSubpage extends LitElement {
|
||||
<div class="toolbar ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar-content">
|
||||
${
|
||||
this.mainPage || (!backPath && history.state?.root)
|
||||
this.mainPage || history.state?.root
|
||||
? html`<ha-menu-button></ha-menu-button>`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${backPath}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: backPath
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
href=${backPath}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
}
|
||||
|
||||
<div class="main-title">
|
||||
@@ -74,8 +79,12 @@ class HassSubpage extends LitElement {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
private _backTapped(ev: MouseEvent): void {
|
||||
handleBackClick(ev, this.backPath, this.backCallback);
|
||||
private _backTapped(): void {
|
||||
if (this.backCallback) {
|
||||
this.backCallback();
|
||||
return;
|
||||
}
|
||||
goBack();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -14,10 +14,9 @@ import { canShowPage } from "../common/config/can_show_page";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { goBack, navigate } from "../common/navigate";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
|
||||
import { handleBackClick } from "./back-navigation";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
import "../components/ha-svg-icon";
|
||||
@@ -176,12 +175,17 @@ export class HassTabsSubpage extends LitElement {
|
||||
${
|
||||
this.mainPage || (!backPath && history.state?.root)
|
||||
? html`<ha-menu-button></ha-menu-button>`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${backPath}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: backPath
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${backPath}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
}
|
||||
${
|
||||
this._narrow || !this.showTabs
|
||||
@@ -242,8 +246,12 @@ export class HassTabsSubpage extends LitElement {
|
||||
this._content.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
private _backTapped(ev: MouseEvent): void {
|
||||
handleBackClick(ev, this.backPath, this.backCallback);
|
||||
private _backTapped(): void {
|
||||
if (this.backCallback) {
|
||||
this.backCallback();
|
||||
return;
|
||||
}
|
||||
goBack();
|
||||
}
|
||||
|
||||
private _isActiveTabPath(tabPath: string, currentPath: string): boolean {
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { popoverSupported } from "../common/feature-detect/support-popover";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-icon-button";
|
||||
@@ -10,7 +23,7 @@ import type { ToastClosedEventDetail } from "../components/ha-toast";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface ShowToastParams {
|
||||
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
|
||||
// Unique ID for updating or closing a specific toast without flickering.
|
||||
id?: string;
|
||||
message:
|
||||
| string
|
||||
@@ -34,105 +47,150 @@ export interface ToastActionParams {
|
||||
| { translationKey: LocalizeKeys; args?: Record<string, string | number> };
|
||||
}
|
||||
|
||||
interface Notification {
|
||||
key: string;
|
||||
parameters: ShowToastParams;
|
||||
}
|
||||
|
||||
@customElement("notification-manager")
|
||||
class NotificationManager extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _parameters?: ShowToastParams;
|
||||
@state() private _notifications: Notification[] = [];
|
||||
|
||||
@query("ha-toast")
|
||||
private _toast!: HTMLElementTagNameMap["ha-toast"] | undefined;
|
||||
@query(".stack") private _stack?: HTMLDivElement;
|
||||
|
||||
private _showDialogId = 0;
|
||||
@queryAll("ha-toast")
|
||||
private _toasts!: NodeListOf<HTMLElementTagNameMap["ha-toast"]>;
|
||||
|
||||
private _anonymousId = 0;
|
||||
|
||||
public async showDialog(parameters: ShowToastParams) {
|
||||
const showId = ++this._showDialogId;
|
||||
|
||||
if (!parameters.id || this._parameters?.id !== parameters.id) {
|
||||
await this._toast?.hide();
|
||||
}
|
||||
|
||||
if (showId !== this._showDialogId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parameters.duration === 0) {
|
||||
this._parameters = undefined;
|
||||
if (parameters.id) {
|
||||
await this._closeNotification(`identified-${parameters.id}`);
|
||||
} else {
|
||||
const notification = [...this._notifications]
|
||||
.reverse()
|
||||
.find(({ parameters: { id } }) => !id);
|
||||
if (notification) {
|
||||
await this._closeNotification(notification.key);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._parameters = parameters;
|
||||
const normalizedParameters = {
|
||||
...parameters,
|
||||
duration:
|
||||
parameters.duration === undefined ||
|
||||
(parameters.duration > 0 && parameters.duration <= 4000)
|
||||
? 4000
|
||||
: parameters.duration,
|
||||
};
|
||||
const key = parameters.id
|
||||
? `identified-${parameters.id}`
|
||||
: `anonymous-${++this._anonymousId}`;
|
||||
const existingIndex = parameters.id
|
||||
? this._notifications.findIndex(
|
||||
(notification) => notification.key === key
|
||||
)
|
||||
: -1;
|
||||
|
||||
if (
|
||||
this._parameters.duration === undefined ||
|
||||
(this._parameters.duration > 0 && this._parameters.duration <= 4000)
|
||||
) {
|
||||
this._parameters.duration = 4000;
|
||||
}
|
||||
this._notifications =
|
||||
existingIndex === -1
|
||||
? [...this._notifications, { key, parameters: normalizedParameters }]
|
||||
: this._notifications.map((notification, index) =>
|
||||
index === existingIndex
|
||||
? { key, parameters: normalizedParameters }
|
||||
: notification
|
||||
);
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
if (showId !== this._showDialogId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast?.show();
|
||||
this._showStack();
|
||||
this._getToast(key)?.show();
|
||||
}
|
||||
|
||||
private _toastClosed(ev: HASSDomEvent<ToastClosedEventDetail>) {
|
||||
private _toastClosed(
|
||||
ev: HASSDomEvent<ToastClosedEventDetail> &
|
||||
HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-toast"]>
|
||||
) {
|
||||
const key = ev.currentTarget.dataset.notificationKey!;
|
||||
if (ev.detail.reason === "dismiss") {
|
||||
this._parameters?.dismiss?.();
|
||||
this._getNotification(key)?.parameters.dismiss?.();
|
||||
}
|
||||
this._parameters = undefined;
|
||||
this._removeNotification(key);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._parameters) {
|
||||
if (!this._notifications.length) {
|
||||
return nothing;
|
||||
}
|
||||
const bottomOffset = Math.max(
|
||||
...this._notifications.map(
|
||||
({ parameters }) => parameters.bottomOffset ?? 0
|
||||
)
|
||||
);
|
||||
return html`
|
||||
<ha-toast
|
||||
.labelText=${
|
||||
typeof this._parameters.message !== "string"
|
||||
? this.hass.localize(
|
||||
this._parameters.message.translationKey,
|
||||
this._parameters.message.args
|
||||
)
|
||||
: this._parameters.message
|
||||
}
|
||||
.announceText=${
|
||||
this._parameters.announceMessage
|
||||
? typeof this._parameters.announceMessage !== "string"
|
||||
? this.hass.localize(
|
||||
this._parameters.announceMessage.translationKey,
|
||||
this._parameters.announceMessage.args
|
||||
)
|
||||
: this._parameters.announceMessage
|
||||
: undefined
|
||||
}
|
||||
.timeoutMs=${this._parameters.duration!}
|
||||
.bottomOffset=${this._parameters.bottomOffset ?? 0}
|
||||
@toast-closed=${this._toastClosed}
|
||||
<div
|
||||
class="stack"
|
||||
style=${styleMap({
|
||||
"--notification-stack-bottom-offset": `${bottomOffset}px`,
|
||||
})}
|
||||
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
|
||||
>
|
||||
${this._renderAction(this._parameters.secondaryAction, true)}
|
||||
${this._renderAction(this._parameters.action, false)}
|
||||
${
|
||||
this._parameters?.dismissable
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
slot="dismiss"
|
||||
@click=${this._dismissClicked}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-toast>
|
||||
${repeat(
|
||||
this._notifications,
|
||||
(notification) => notification.key,
|
||||
({ key, parameters }) => html`
|
||||
<ha-toast
|
||||
data-notification-key=${key}
|
||||
.labelText=${
|
||||
typeof parameters.message !== "string"
|
||||
? this.hass.localize(
|
||||
parameters.message.translationKey,
|
||||
parameters.message.args
|
||||
)
|
||||
: parameters.message
|
||||
}
|
||||
.announceText=${
|
||||
parameters.announceMessage
|
||||
? typeof parameters.announceMessage !== "string"
|
||||
? this.hass.localize(
|
||||
parameters.announceMessage.translationKey,
|
||||
parameters.announceMessage.args
|
||||
)
|
||||
: parameters.announceMessage
|
||||
: undefined
|
||||
}
|
||||
.timeoutMs=${parameters.duration!}
|
||||
.stacked=${true}
|
||||
@toast-closed=${this._toastClosed}
|
||||
>
|
||||
${this._renderAction(key, parameters.secondaryAction, true)}
|
||||
${this._renderAction(key, parameters.action, false)}
|
||||
${
|
||||
parameters.dismissable
|
||||
? html`
|
||||
<ha-icon-button
|
||||
data-notification-key=${key}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
slot="dismiss"
|
||||
@click=${this._dismissClicked}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-toast>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAction(
|
||||
key: string,
|
||||
action: ToastActionParams | undefined,
|
||||
secondary: boolean
|
||||
) {
|
||||
@@ -141,6 +199,7 @@ class NotificationManager extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<ha-button
|
||||
data-notification-key=${key}
|
||||
appearance=${action.primary ? "filled" : "plain"}
|
||||
size="s"
|
||||
slot="action"
|
||||
@@ -155,19 +214,99 @@ class NotificationManager extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _buttonClicked() {
|
||||
this._toast?.hide("action");
|
||||
this._parameters?.action?.action();
|
||||
private _buttonClicked(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-button"]>
|
||||
) {
|
||||
const key = ev.currentTarget.dataset.notificationKey!;
|
||||
this._getToast(key)?.hide("action");
|
||||
this._getNotification(key)?.parameters.action?.action();
|
||||
}
|
||||
|
||||
private _secondaryButtonClicked() {
|
||||
this._toast?.hide("action");
|
||||
this._parameters?.secondaryAction?.action();
|
||||
private _secondaryButtonClicked(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-button"]>
|
||||
) {
|
||||
const key = ev.currentTarget.dataset.notificationKey!;
|
||||
this._getToast(key)?.hide("action");
|
||||
this._getNotification(key)?.parameters.secondaryAction?.action();
|
||||
}
|
||||
|
||||
private _dismissClicked() {
|
||||
this._toast?.hide("dismiss");
|
||||
private _dismissClicked(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-icon-button"]>
|
||||
) {
|
||||
this._getToast(ev.currentTarget.dataset.notificationKey!)?.hide("dismiss");
|
||||
}
|
||||
|
||||
private _getNotification(key: string) {
|
||||
return this._notifications.find((notification) => notification.key === key);
|
||||
}
|
||||
|
||||
private _getToast(key: string) {
|
||||
return [...this._toasts].find(
|
||||
(toast) => toast.dataset.notificationKey === key
|
||||
);
|
||||
}
|
||||
|
||||
private async _closeNotification(key: string) {
|
||||
const notification = this._getNotification(key);
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
await this._getToast(key)?.hide();
|
||||
if (this._getNotification(key) === notification) {
|
||||
this._removeNotification(key);
|
||||
}
|
||||
}
|
||||
|
||||
private _removeNotification(key: string) {
|
||||
this._notifications = this._notifications.filter(
|
||||
(notification) => notification.key !== key
|
||||
);
|
||||
if (!this._notifications.length) {
|
||||
this._hideStack();
|
||||
}
|
||||
}
|
||||
|
||||
private _showStack() {
|
||||
if (!popoverSupported || !this._stack) {
|
||||
return;
|
||||
}
|
||||
// Top-layer order is order of entry — re-enter so we paint above any
|
||||
// dialog backdrop opened since the stack was first shown.
|
||||
if (this._stack.matches(":popover-open")) {
|
||||
this._stack.hidePopover();
|
||||
}
|
||||
this._stack.showPopover();
|
||||
}
|
||||
|
||||
private _hideStack() {
|
||||
if (!popoverSupported || !this._stack?.matches(":popover-open")) {
|
||||
return;
|
||||
}
|
||||
this._stack.hidePopover();
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
.stack {
|
||||
position: fixed;
|
||||
inset-block-start: auto;
|
||||
inset-inline-end: auto;
|
||||
inset-block-end: calc(
|
||||
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4) +
|
||||
var(--notification-stack-bottom-offset, 0px)
|
||||
);
|
||||
inset-inline-start: 50%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
transform: translateX(calc(-50% * var(--scale-direction)));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
||||
@@ -455,7 +456,7 @@ class DialogAreaDetail
|
||||
return deviceReg && deviceReg.area_id === areaId;
|
||||
};
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
private _nameChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
this._updateDirtyState(this._currentState());
|
||||
@@ -479,7 +480,9 @@ class DialogAreaDetail
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
|
||||
private _pictureChanged(
|
||||
ev: ValueChangedEvent<string | null> & HASSDomTargetEvent<HaPictureUpload>
|
||||
) {
|
||||
this._error = undefined;
|
||||
this._picture = (ev.target as HaPictureUpload).value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
@@ -490,7 +493,9 @@ class DialogAreaDetail
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _sensorChanged(ev: CustomEvent): void {
|
||||
private _sensorChanged(
|
||||
ev: ValueChangedEvent<string> & HASSDomTargetEvent<HaEntityPicker>
|
||||
): void {
|
||||
const deviceClass = (ev.target as HaEntityPicker).includeDeviceClasses![0];
|
||||
const key = `_${deviceClass}Entity`;
|
||||
this[key] = ev.detail.value || null;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/chips/ha-chip-set";
|
||||
import "../../../components/chips/ha-input-chip";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -336,13 +337,13 @@ class DialogFloorDetail extends DirtyStateProviderMixin<FloorFormState>()(
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
private _nameChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _levelChanged(ev: InputEvent) {
|
||||
private _levelChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
|
||||
this._error = undefined;
|
||||
this._level =
|
||||
(ev.target as HaInput).value === ""
|
||||
|
||||
@@ -644,7 +644,6 @@ class HaConfigAreaPage extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/areas/dashboard"
|
||||
.header=${html`${
|
||||
area.icon
|
||||
? html`<ha-icon
|
||||
|
||||
@@ -86,6 +86,8 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
@state() private _hierarchy?: AreasFloorHierarchy;
|
||||
|
||||
private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
private _blockHierarchyUpdate = false;
|
||||
|
||||
private _blockHierarchyUpdateTimeout?: number;
|
||||
@@ -166,7 +168,9 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.tabs=${configSections.areas}
|
||||
.route=${this.route}
|
||||
has-fab
|
||||
|
||||
@@ -443,7 +443,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
id="entity_id"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
@@ -110,7 +110,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/automation/dashboard"
|
||||
.header=${title}
|
||||
.scrollable=${this.narrow}
|
||||
>
|
||||
@@ -453,7 +452,11 @@ export class HaAutomationTrace extends LitElement {
|
||||
if (runId) {
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.delete("run_id");
|
||||
replaceCurrentUrl(`${location.pathname}?${params.toString()}`);
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${location.pathname}?${params.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
await showAlertDialog(this, {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import {
|
||||
extractSearchParam,
|
||||
@@ -36,6 +35,8 @@ import type HaAutomationSidebar from "./ha-automation-sidebar";
|
||||
|
||||
export const SIDEBAR_DEFAULT_WIDTH = 500;
|
||||
|
||||
export const PASTED_CONFIG_TOAST_ID = "pasted-config";
|
||||
|
||||
export const ManualEditorMixin = <TConfig>(
|
||||
superClass: Constructor<LitElement>
|
||||
) => {
|
||||
@@ -172,7 +173,11 @@ export const ManualEditorMixin = <TConfig>(
|
||||
}
|
||||
|
||||
protected clearParam(param: string) {
|
||||
replaceCurrentUrl(constructUrlCurrentPath(removeSearchParam(param)));
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam(param))
|
||||
);
|
||||
}
|
||||
|
||||
protected async openSidebar(ev: CustomEvent<SidebarConfig>) {
|
||||
@@ -234,6 +239,7 @@ export const ManualEditorMixin = <TConfig>(
|
||||
this.pastedConfig = undefined;
|
||||
|
||||
showToast(this, {
|
||||
id: PASTED_CONFIG_TOAST_ID,
|
||||
message: "",
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
@@ -37,7 +37,10 @@ import "./action/ha-automation-action";
|
||||
import type HaAutomationAction from "./action/ha-automation-action";
|
||||
import "./condition/ha-automation-condition";
|
||||
import type HaAutomationCondition from "./condition/ha-automation-condition";
|
||||
import { ManualEditorMixin } from "./ha-manual-editor-mixin";
|
||||
import {
|
||||
ManualEditorMixin,
|
||||
PASTED_CONFIG_TOAST_ID,
|
||||
} from "./ha-manual-editor-mixin";
|
||||
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { manualEditorStyles, saveFabStyles } from "./styles";
|
||||
import "./trigger/ha-automation-trigger";
|
||||
@@ -431,6 +434,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
|
||||
protected showPastedToastWithUndo() {
|
||||
showEditorToast(this, {
|
||||
id: PASTED_CONFIG_TOAST_ID,
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.paste_toast_message"
|
||||
),
|
||||
|
||||
@@ -74,6 +74,8 @@ class HaConfigBackupOverview extends LitElement {
|
||||
|
||||
@state() private _config?: BackupConfig;
|
||||
|
||||
private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("config") && !this._config) {
|
||||
@@ -204,7 +206,9 @@ class HaConfigBackupOverview extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config/system"
|
||||
}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.backup.overview.header")}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -119,7 +118,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
}
|
||||
|
||||
private _clearHash() {
|
||||
replaceCurrentUrl(window.location.pathname);
|
||||
history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mdiClose, mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { withViewTransition } from "../../../common/util/view-transition";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
@@ -279,7 +280,7 @@ class DialogImportBlueprint extends DirtyStateProviderMixin<BlueprintImportState
|
||||
});
|
||||
}
|
||||
|
||||
private _inputChanged(ev: Event) {
|
||||
private _inputChanged(ev: HASSDomTargetEvent<HaInput>) {
|
||||
this._updateDirtyState({
|
||||
value: (ev.target as HaInput).value ?? "",
|
||||
hasResult: !!this._result,
|
||||
|
||||
@@ -13,7 +13,10 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@@ -483,7 +486,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
this._createNew(blueprint);
|
||||
}
|
||||
|
||||
private _handleUsageClick = (ev: Event) => {
|
||||
private _handleUsageClick = (ev: HASSDomCurrentTargetEvent<HTMLElement>) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const target = ev.currentTarget as HTMLElement | null;
|
||||
|
||||
@@ -68,6 +68,7 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
|
||||
@@ -21,7 +21,6 @@ export class CloudForgotPassword extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.cloud.forgot_password.title"
|
||||
)}
|
||||
|
||||
@@ -45,7 +45,6 @@ export class CloudLoginPanel extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
|
||||
@@ -38,7 +38,6 @@ export class CloudRegister extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize("ui.panel.config.cloud.register.title")}
|
||||
>
|
||||
<div class="content">
|
||||
|
||||
@@ -66,6 +66,8 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _showSkipped = false;
|
||||
|
||||
@state() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
@@ -153,7 +155,9 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config/system"
|
||||
}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.updates.caption")}
|
||||
|
||||
@@ -986,7 +986,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/devices/dashboard"
|
||||
.header=${deviceName}
|
||||
>
|
||||
<ha-tooltip for="edit-settings-button" slot="toolbar-icon">
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate, updateHistoryState } from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
@@ -778,7 +778,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.tabs=${configSections.devices}
|
||||
.route=${this.route}
|
||||
.searchLabel=${this.hass.localize(
|
||||
@@ -1041,7 +1043,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
updateHistoryState({ filter: this._filter });
|
||||
history.replaceState({ filter: this._filter }, "");
|
||||
}
|
||||
|
||||
private _addDevice() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
@@ -340,7 +341,7 @@ export class DialogEnergyBatterySettings
|
||||
}
|
||||
|
||||
private _handlePowerConfigChanged(
|
||||
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
|
||||
ev: HASSDomEvent<HASSDomEvents["power-config-changed"]>
|
||||
) {
|
||||
this._powerType = ev.detail.powerType;
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -214,7 +215,7 @@ export class DialogEnergyCustomise
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleCard = (ev: Event): void => {
|
||||
private _toggleCard = (ev: HASSDomCurrentTargetEvent<HaSwitch>): void => {
|
||||
const target = ev.currentTarget as HaSwitch;
|
||||
const cardKey = target.dataset.cardKey;
|
||||
if (!cardKey) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -353,7 +354,7 @@ export class DialogEnergyGasSettings
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleCostChanged(ev: Event) {
|
||||
private _handleCostChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -593,7 +597,9 @@ export class DialogEnergyGridSettings
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handleImportCostTypeChanged(ev: Event) {
|
||||
private _handleImportCostTypeChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
|
||||
) {
|
||||
this._importCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
// Clear other cost fields when switching types
|
||||
this._source = {
|
||||
@@ -605,7 +611,9 @@ export class DialogEnergyGridSettings
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handleExportCostTypeChanged(ev: Event) {
|
||||
private _handleExportCostTypeChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
|
||||
) {
|
||||
this._exportCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
// Clear other cost fields when switching types
|
||||
this._source = {
|
||||
@@ -630,9 +638,10 @@ export class DialogEnergyGridSettings
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _numberCostChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HTMLInputElement;
|
||||
const value = input.value ? parseFloat(input.value) : null;
|
||||
private _numberCostChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
|
||||
const value = ev.currentTarget.value
|
||||
? parseFloat(ev.currentTarget.value)
|
||||
: null;
|
||||
this._source = { ...this._source!, number_energy_price: value };
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
@@ -653,15 +662,16 @@ export class DialogEnergyGridSettings
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _numberCompensationChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HTMLInputElement;
|
||||
const value = input.value ? parseFloat(input.value) : null;
|
||||
private _numberCompensationChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
|
||||
const value = ev.currentTarget.value
|
||||
? parseFloat(ev.currentTarget.value)
|
||||
: null;
|
||||
this._source = { ...this._source!, number_energy_price_export: value };
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handlePowerConfigChanged(
|
||||
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
|
||||
ev: HASSDomEvent<HASSDomEvents["power-config-changed"]>
|
||||
) {
|
||||
this._powerType = ev.detail.powerType;
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-checkbox";
|
||||
@@ -12,6 +13,7 @@ import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/input/ha-input";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
@@ -234,7 +236,7 @@ export class DialogEnergySolarSettings
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>${entry.title}
|
||||
/>${entry.title || domainToName(this.hass.localize, entry.domain)}
|
||||
</div>
|
||||
</ha-checkbox>`
|
||||
)}
|
||||
@@ -290,7 +292,7 @@ export class DialogEnergySolarSettings
|
||||
);
|
||||
}
|
||||
|
||||
private _handleForecastChanged(ev: Event) {
|
||||
private _handleForecastChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
|
||||
this._forecast = (ev.currentTarget as HaRadioGroup).value === "true";
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -289,7 +290,7 @@ export class DialogEnergyWaterSettings
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleCostChanged(ev: Event) {
|
||||
private _handleCostChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
@@ -234,7 +235,7 @@ export class HaEnergyPowerConfig extends LitElement {
|
||||
fireEvent(this, "hass-more-info", { entityId: this.helperEntityId! });
|
||||
}
|
||||
|
||||
private _handlePowerTypeChanged(ev: Event) {
|
||||
private _handlePowerTypeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
|
||||
const newPowerType = (ev.currentTarget as HaRadioGroup).value as PowerType;
|
||||
// Clear power config when switching types
|
||||
fireEvent(this, "power-config-changed", {
|
||||
|
||||
@@ -78,6 +78,8 @@ class HaConfigEnergy extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _info?: EnergyInfo;
|
||||
|
||||
@state() private _preferences?: EnergyPreferences;
|
||||
@@ -124,7 +126,11 @@ class HaConfigEnergy extends LitElement {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
back-path="/config/lovelace/dashboards"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config/lovelace/dashboards"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${TABS}
|
||||
>
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { updateHistoryState } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
@@ -811,7 +810,9 @@ export class HaConfigEntities extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.hass.localize, filteredEntities)}
|
||||
@@ -1245,7 +1246,7 @@ export class HaConfigEntities extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
updateHistoryState({ filter: this._filter });
|
||||
history.replaceState({ filter: this._filter }, "");
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
|
||||
@@ -106,6 +106,7 @@ class HaPanelConfig extends HassRouterPage {
|
||||
integrations: {
|
||||
tag: "ha-config-integrations",
|
||||
load: () => import("./integrations/ha-config-integrations"),
|
||||
waitForReady: true,
|
||||
},
|
||||
labels: {
|
||||
tag: "ha-config-labels",
|
||||
|
||||
@@ -4,6 +4,9 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/input/ha-input";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import type { InputBoolean } from "../../../../data/input_boolean";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -22,6 +25,8 @@ class HaInputBooleanForm extends LitElement {
|
||||
|
||||
@state() private _icon!: string;
|
||||
|
||||
@state() private _initial?: boolean;
|
||||
|
||||
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
|
||||
|
||||
set item(item: InputBoolean) {
|
||||
@@ -29,9 +34,11 @@ class HaInputBooleanForm extends LitElement {
|
||||
if (item) {
|
||||
this._name = item.name || "";
|
||||
this._icon = item.icon || "";
|
||||
this._initial = item.initial;
|
||||
} else {
|
||||
this._name = "";
|
||||
this._icon = "";
|
||||
this._initial = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +77,58 @@ class HaInputBooleanForm extends LitElement {
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
></ha-icon-picker>
|
||||
<ha-radio-group
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_boolean.initial"
|
||||
)}
|
||||
.value=${
|
||||
this._initial === undefined
|
||||
? "restore"
|
||||
: this._initial
|
||||
? "on"
|
||||
: "off"
|
||||
}
|
||||
.disabled=${this.disabled}
|
||||
name="initial"
|
||||
@change=${this._initialChanged}
|
||||
>
|
||||
<ha-radio-option value="restore">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_boolean.restore"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="on">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_boolean.turn_on"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="off">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_boolean.turn_off"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _initialChanged(ev: Event) {
|
||||
const option = String((ev.currentTarget as HaRadioGroup).value);
|
||||
const initial = option === "restore" ? undefined : option === "on";
|
||||
if (initial === this._initial) {
|
||||
return;
|
||||
}
|
||||
const newValue = { ...this._item } as InputBoolean;
|
||||
if (initial === undefined) {
|
||||
delete newValue.initial;
|
||||
} else {
|
||||
newValue.initial = initial;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
if (!this.new && !this._item) {
|
||||
return;
|
||||
@@ -108,6 +163,9 @@ class HaInputBooleanForm extends LitElement {
|
||||
ha-input {
|
||||
margin: var(--ha-space-2) 0;
|
||||
}
|
||||
ha-radio-group {
|
||||
margin-top: var(--ha-space-5);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -644,7 +644,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.searchLabel=${this.hass.localize(
|
||||
|
||||
@@ -373,11 +373,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
: this._manifest?.documentation;
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/integrations/dashboard"
|
||||
>
|
||||
<hass-subpage .hass=${this.hass} .narrow=${this.narrow}>
|
||||
${
|
||||
documentationLink
|
||||
? html`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate, updateHistoryState } from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
@@ -54,6 +55,10 @@ import type { ImprovDiscoveredDevice } from "../../../external_app/external_mess
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import type { HassTabsSubpage } from "../../../layouts/hass-tabs-subpage";
|
||||
import {
|
||||
childPanelReadyContext,
|
||||
type RegisterChildPanelReady,
|
||||
} from "../../../layouts/panel-ready";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -158,6 +163,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
window.location.hash.substring(1)
|
||||
);
|
||||
|
||||
@state() private _searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _filter: string = history.state?.filter || "";
|
||||
|
||||
@state() private _logInfos?: Record<string, IntegrationLogInfo>;
|
||||
@@ -166,6 +173,17 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
|
||||
@query("hass-tabs-subpage") private _tabsSubpage?: HassTabsSubpage;
|
||||
|
||||
private _resolveInitialRender?: () => void;
|
||||
|
||||
private _initialRenderComplete = new Promise<void>((resolve) => {
|
||||
this._resolveInitialRender = resolve;
|
||||
});
|
||||
|
||||
private _childReadyRegistered = false;
|
||||
|
||||
@consume({ context: childPanelReadyContext, subscribe: true })
|
||||
private _registerChildPanelReady?: RegisterChildPanelReady;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(
|
||||
@@ -386,6 +404,10 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
super.updated(changed);
|
||||
if (!this._childReadyRegistered && this._registerChildPanelReady) {
|
||||
this._registerChildPanelReady(this._initialRenderComplete);
|
||||
this._childReadyRegistered = true;
|
||||
}
|
||||
if (changed.has("route")) {
|
||||
this._handleRouteChanged();
|
||||
}
|
||||
@@ -413,6 +435,9 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
}
|
||||
|
||||
if (this.configEntries && this.configEntriesInProgress) {
|
||||
this._resolveInitialRender?.();
|
||||
this._resolveInitialRender = undefined;
|
||||
|
||||
const activeElement = deepActiveElement();
|
||||
|
||||
if (
|
||||
@@ -501,7 +526,9 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
back-path="/config"
|
||||
.backPath=${
|
||||
this._searchParams.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
has-fab
|
||||
@@ -858,7 +885,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
this._filter = (ev.target as HaInputSearch).value ?? "";
|
||||
updateHistoryState({ filter: this._filter });
|
||||
history.replaceState({ filter: this._filter }, "");
|
||||
}
|
||||
|
||||
private async _highlightEntry() {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import { ChildPanelReady } from "../../../layouts/panel-ready";
|
||||
import type { RouterOptions } from "../../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../layouts/hass-router-page";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
@@ -70,6 +71,11 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
|
||||
|
||||
private _loadTranslationsPromise?: Promise<LocalizeFunc>;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
new ChildPanelReady(this);
|
||||
}
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeConfigEntries(
|
||||
|
||||
+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 };
|
||||
|
||||
@@ -95,7 +95,6 @@ export class DHCPConfigPanel extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config/integrations/integration/dhcp"
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._dataWithIds(this._data)}
|
||||
.noDataText=${this.hass.localize(
|
||||
|
||||
+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;
|
||||
|
||||
@@ -61,11 +61,7 @@ export class MQTTConfigPanel extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
back-path="/config/integrations/integration/mqtt"
|
||||
>
|
||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize("ui.panel.config.mqtt.settings_title")}
|
||||
|
||||
@@ -99,7 +99,6 @@ export class SSDPConfigPanel extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config/integrations/integration/ssdp"
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
|
||||
@@ -106,7 +106,6 @@ export class ZeroconfConfigPanel extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config/integrations/integration/zeroconf"
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
|
||||
@@ -76,7 +76,6 @@ class ZHAAddDevicesPage extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/zha/dashboard"
|
||||
.header=${this.hass.localize("ui.panel.config.zha.add_device")}
|
||||
>
|
||||
<ha-button
|
||||
|
||||
@@ -11,10 +11,7 @@ import { addGroup, fetchGroupableDevices } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "./zha-device-endpoint-list";
|
||||
import type {
|
||||
DeviceEndpointSelectionChangedEvent,
|
||||
ZHADeviceEndpointList,
|
||||
} from "./zha-device-endpoint-list";
|
||||
import type { ZHADeviceEndpointList } from "./zha-device-endpoint-list";
|
||||
|
||||
@customElement("zha-add-group-page")
|
||||
export class ZHAAddGroupPage extends LitElement {
|
||||
@@ -131,7 +128,7 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
}
|
||||
|
||||
private _handleAddSelectionChanged(
|
||||
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
|
||||
ev: HASSDomEvent<HASSDomEvents["selection-changed"]>
|
||||
): void {
|
||||
this._selectedDevicesToAdd = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,15 @@ import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../../../components/input/ha-input-search";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
|
||||
@@ -269,11 +274,16 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
private _handleFilterChanged(ev: Event): void {
|
||||
this._filter = (ev.currentTarget as HTMLInputElement).value;
|
||||
private _handleFilterChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaInputSearch>
|
||||
): void {
|
||||
this._filter = ev.currentTarget.value ?? "";
|
||||
}
|
||||
|
||||
private _handleItemSelected(ev: CustomEvent<number>): void {
|
||||
private _handleItemSelected(
|
||||
ev: HASSDomEvent<HASSDomEvents["ha-list-item-selected"]> &
|
||||
HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
): void {
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
let selectedDeviceIds = this._selectedDeviceIds;
|
||||
|
||||
@@ -290,7 +300,10 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleItemDeselected(ev: CustomEvent<number>): void {
|
||||
private _handleItemDeselected(
|
||||
ev: HASSDomEvent<HASSDomEvents["ha-list-item-deselected"]> &
|
||||
HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
): void {
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
let selectedDeviceIds = this._selectedDeviceIds;
|
||||
|
||||
|
||||
@@ -19,10 +19,7 @@ import "../../../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { formatAsPaddedHex } from "./functions";
|
||||
import "./zha-device-endpoint-list";
|
||||
import type {
|
||||
DeviceEndpointSelectionChangedEvent,
|
||||
ZHADeviceEndpointList,
|
||||
} from "./zha-device-endpoint-list";
|
||||
import type { ZHADeviceEndpointList } from "./zha-device-endpoint-list";
|
||||
import { showZHAAddGroupMembersDialog } from "./show-dialog-zha-add-group-members";
|
||||
|
||||
@customElement("zha-group-page")
|
||||
@@ -202,7 +199,7 @@ export class ZHAGroupPage extends LitElement {
|
||||
}
|
||||
|
||||
private _handleRemoveSelectionChanged(
|
||||
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
|
||||
ev: HASSDomEvent<HASSDomEvents["selection-changed"]>
|
||||
): void {
|
||||
this._selectedDevicesToRemove = ev.detail.value;
|
||||
}
|
||||
|
||||
-1
@@ -55,7 +55,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/zha/dashboard"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.header"
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import "../../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../../components/ha-switch";
|
||||
import type { ZHAConfiguration } from "../../../../../data/zha";
|
||||
import {
|
||||
fetchZHAConfiguration,
|
||||
@@ -18,6 +21,8 @@ import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
const PREDEFINED_TIMEOUTS = [1800, 3600, 7200, 21600, 43200, 86400];
|
||||
type ZHASwitchChangeEvent = HASSDomCurrentTargetEvent<HaSwitch>;
|
||||
type ZHAInputChangeEvent = HASSDomCurrentTargetEvent<HaInput>;
|
||||
|
||||
@customElement("zha-options-page")
|
||||
class ZHAOptionsPage extends LitElement {
|
||||
@@ -345,53 +350,54 @@ class ZHAOptionsPage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _enableIdentifyOnJoinChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.enable_identify_on_join = checked;
|
||||
private _enableIdentifyOnJoinChanged(ev: ZHASwitchChangeEvent): void {
|
||||
this._configuration!.data.zha_options.enable_identify_on_join =
|
||||
ev.currentTarget.checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _enhancedLightTransitionChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.enhanced_light_transition = checked;
|
||||
private _enhancedLightTransitionChanged(ev: ZHASwitchChangeEvent): void {
|
||||
this._configuration!.data.zha_options.enhanced_light_transition =
|
||||
ev.currentTarget.checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _lightTransitioningFlagChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.light_transitioning_flag = checked;
|
||||
private _lightTransitioningFlagChanged(ev: ZHASwitchChangeEvent): void {
|
||||
this._configuration!.data.zha_options.light_transitioning_flag =
|
||||
ev.currentTarget.checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _groupMembersAssumeStateChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.group_members_assume_state = checked;
|
||||
private _groupMembersAssumeStateChanged(ev: ZHASwitchChangeEvent): void {
|
||||
this._configuration!.data.zha_options.group_members_assume_state =
|
||||
ev.currentTarget.checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _enableMainsStartupPollingChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
private _enableMainsStartupPollingChanged(ev: ZHASwitchChangeEvent): void {
|
||||
this._configuration!.data.zha_options.enable_mains_startup_polling =
|
||||
checked;
|
||||
ev.currentTarget.checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _defaultLightTransitionChanged(ev: Event): void {
|
||||
const value = Number((ev.target as HTMLInputElement).value);
|
||||
this._configuration!.data.zha_options.default_light_transition = value;
|
||||
private _defaultLightTransitionChanged(ev: ZHAInputChangeEvent): void {
|
||||
this._configuration!.data.zha_options.default_light_transition = Number(
|
||||
ev.currentTarget.value
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _customMainsSecondsChanged(ev: Event): void {
|
||||
const seconds = Number((ev.target as HTMLInputElement).value);
|
||||
this._configuration!.data.zha_options.consider_unavailable_mains = seconds;
|
||||
private _customMainsSecondsChanged(ev: ZHAInputChangeEvent): void {
|
||||
this._configuration!.data.zha_options.consider_unavailable_mains = Number(
|
||||
ev.currentTarget.value
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _customBatterySecondsChanged(ev: Event): void {
|
||||
const seconds = Number((ev.target as HTMLInputElement).value);
|
||||
this._configuration!.data.zha_options.consider_unavailable_battery =
|
||||
seconds;
|
||||
private _customBatterySecondsChanged(ev: ZHAInputChangeEvent): void {
|
||||
this._configuration!.data.zha_options.consider_unavailable_battery = Number(
|
||||
ev.currentTarget.value
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -419,7 +425,15 @@ class ZHAOptionsPage extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private async _updateConfiguration(ev: Event): Promise<void> {
|
||||
private async _updateConfiguration(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & {
|
||||
progress: boolean;
|
||||
actionSuccess: () => void;
|
||||
actionError: () => void;
|
||||
}
|
||||
>
|
||||
): Promise<void> {
|
||||
const button = ev.currentTarget as HTMLElement & {
|
||||
progress: boolean;
|
||||
actionSuccess: () => void;
|
||||
|
||||
+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
-12
@@ -34,12 +34,7 @@ class ZWaveJSConfigEntryPicker extends LitElement {
|
||||
|
||||
if (this._configEntries.length === 0) {
|
||||
return html`
|
||||
<hass-subpage
|
||||
header="Z-Wave"
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
back-path="/config/integrations/integration/zwave_js"
|
||||
>
|
||||
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
@@ -56,12 +51,7 @@ class ZWaveJSConfigEntryPicker extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
header="Z-Wave"
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
back-path="/config/integrations/integration/zwave_js"
|
||||
>
|
||||
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
|
||||
@@ -2,11 +2,13 @@ import { mdiCloseCircle } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import {
|
||||
getZwaveNodeRawConfigParameter,
|
||||
setZwaveNodeRawConfigParameter,
|
||||
@@ -129,9 +131,9 @@ class ZWaveJSCustomParam extends LitElement {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private _customParamNumberChanged(ev: Event) {
|
||||
private _customParamNumberChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
|
||||
this._customParamNumber = this._tryParseNumber(
|
||||
(ev.target as HTMLInputElement).value
|
||||
ev.currentTarget.value ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,8 +141,8 @@ class ZWaveJSCustomParam extends LitElement {
|
||||
this._valueSize = this._tryParseNumber(ev.detail.value) ?? 1;
|
||||
}
|
||||
|
||||
private _customValueChanged(ev: Event) {
|
||||
this._value = this._tryParseNumber((ev.target as HTMLInputElement).value);
|
||||
private _customValueChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
|
||||
this._value = this._tryParseNumber(ev.currentTarget.value ?? "");
|
||||
}
|
||||
|
||||
private _customValueFormatChanged(ev: HaSelectSelectEvent) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -283,7 +284,9 @@ export class SystemLogCard extends LitElement {
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
}
|
||||
|
||||
private _openLog(ev: Event): void {
|
||||
private _openLog(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { logItem: LoggedError }>
|
||||
): void {
|
||||
const item = (ev.currentTarget as any).logItem;
|
||||
showSystemLogDetailDialog(this, { item });
|
||||
}
|
||||
|
||||
@@ -175,7 +175,6 @@ export class HaConfigLovelaceResources extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config/lovelace/dashboards"
|
||||
.tabs=${lovelaceResourcesTabs}
|
||||
.columns=${this._columns(this.hass.language, this.hass.localize)}
|
||||
.data=${this._resources}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { mainWindow } from "../../../common/dom/get_main_window";
|
||||
import { HOSTNAME_PATTERN } from "../../../common/string/is_hostname";
|
||||
import {
|
||||
IP_ADDRESS_OR_NETWORK_PATTERN,
|
||||
IP_ADDRESS_PATTERN,
|
||||
@@ -38,7 +39,16 @@ const SCHEMA = memoizeOne(
|
||||
{
|
||||
name: "server_port",
|
||||
required: true,
|
||||
selector: { number: { min: 1, max: 65535, mode: "box" } },
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: 65535,
|
||||
mode: "box",
|
||||
validation_message: localize(
|
||||
"ui.panel.config.network.http.invalid_port"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ssl",
|
||||
@@ -118,7 +128,16 @@ const SCHEMA = memoizeOne(
|
||||
{
|
||||
name: "login_attempts_threshold",
|
||||
required: true,
|
||||
selector: { number: { min: -1, max: 1000, mode: "box" } },
|
||||
selector: {
|
||||
number: {
|
||||
min: -1,
|
||||
max: 1000,
|
||||
mode: "box",
|
||||
validation_message: localize(
|
||||
"ui.panel.config.network.http.invalid_login_attempts_threshold"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -133,7 +152,7 @@ const SCHEMA = memoizeOne(
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
pattern: IP_ADDRESS_PATTERN,
|
||||
pattern: `${IP_ADDRESS_PATTERN}|${HOSTNAME_PATTERN}`,
|
||||
validation_message: localize(
|
||||
"ui.panel.config.network.http.invalid_host"
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { isIPAddress } from "../../../common/string/is_ip_address";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
@@ -154,14 +155,12 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize("ui.panel.config.url.description")}
|
||||
</div>
|
||||
|
||||
<h4>
|
||||
${this.hass.localize("ui.panel.config.url.external_url_label")}
|
||||
</h4>
|
||||
${
|
||||
hasCloud
|
||||
? html`
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.external_url_label"
|
||||
)}
|
||||
</h4>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
@@ -363,12 +362,12 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleCloud(ev: Event) {
|
||||
private _toggleCloud(ev: HASSDomCurrentTargetEvent<HaSwitch>) {
|
||||
this._cloudChecked = (ev.currentTarget as HaSwitch).checked;
|
||||
this._showCustomExternalUrl = !this._cloudChecked;
|
||||
}
|
||||
|
||||
private _toggleInternalAutomatic(ev: Event) {
|
||||
private _toggleInternalAutomatic(ev: HASSDomCurrentTargetEvent<HaSwitch>) {
|
||||
this._showCustomInternalUrl = !(ev.currentTarget as HaSwitch).checked;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ import { mdiDeleteOutline, mdiMenuDown, mdiPlus, mdiWifi } from "@mdi/js";
|
||||
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
@@ -630,7 +634,9 @@ export class HassioNetwork extends LitElement {
|
||||
this._interface = { ...this._interfaces[this._curTabIndex] };
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: Event): void {
|
||||
private _handleRadioValueChanged(
|
||||
ev: HASSDomCurrentTargetEvent<HaRadioGroup & { version: "ipv4" | "ipv6" }>
|
||||
): void {
|
||||
const source = ev.currentTarget as HaRadioGroup;
|
||||
const value = source.value as "disabled" | "auto" | "static";
|
||||
const version = (source as any).version as "ipv4" | "ipv6";
|
||||
@@ -645,7 +651,9 @@ export class HassioNetwork extends LitElement {
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _handleRadioValueChangedAp(ev: Event): void {
|
||||
private _handleRadioValueChangedAp(
|
||||
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
|
||||
): void {
|
||||
const source = ev.currentTarget as HaRadioGroup;
|
||||
const value = source.value as "open" | "wep" | "wpa-psk";
|
||||
this._wifiConfiguration!.auth = value;
|
||||
@@ -653,7 +661,11 @@ export class HassioNetwork extends LitElement {
|
||||
this.requestUpdate("_wifiConfiguration");
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(ev: Event): void {
|
||||
private _handleInputValueChanged(
|
||||
ev: HASSDomTargetEvent<
|
||||
HaInput & { version: "ipv4" | "ipv6"; index: number }
|
||||
>
|
||||
): void {
|
||||
const source = ev.target as HaInput;
|
||||
const value = source.value;
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
@@ -691,7 +703,7 @@ export class HassioNetwork extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInputValueChangedWifi(ev: Event): void {
|
||||
private _handleInputValueChangedWifi(ev: HASSDomTargetEvent<HaInput>): void {
|
||||
const source = ev.target as HaInput;
|
||||
const value = source.value;
|
||||
const id = source.id;
|
||||
@@ -708,7 +720,9 @@ export class HassioNetwork extends LitElement {
|
||||
this._wifiConfiguration![id] = value;
|
||||
}
|
||||
|
||||
private _addAddress(ev: Event): void {
|
||||
private _addAddress(
|
||||
ev: HASSDomTargetEvent<HTMLElement & { version: "ipv4" | "ipv6" }>
|
||||
): void {
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
this._interface![version]!.address!.push(
|
||||
version === "ipv4" ? "0.0.0.0/24" : "::/64"
|
||||
@@ -717,7 +731,11 @@ export class HassioNetwork extends LitElement {
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _removeAddress(ev: Event): void {
|
||||
private _removeAddress(
|
||||
ev: HASSDomTargetEvent<
|
||||
HTMLElement & { index: number; version: "ipv4" | "ipv6" }
|
||||
>
|
||||
): void {
|
||||
const source = ev.target as any;
|
||||
const index = source.index as number;
|
||||
const version = source.version as "ipv4" | "ipv6";
|
||||
@@ -752,7 +770,11 @@ export class HassioNetwork extends LitElement {
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _removeNameserver(ev: Event): void {
|
||||
private _removeNameserver(
|
||||
ev: HASSDomTargetEvent<
|
||||
HTMLElement & { index: number; version: "ipv4" | "ipv6" }
|
||||
>
|
||||
): void {
|
||||
const source = ev.target as any;
|
||||
const index = source.index as number;
|
||||
const version = source.version as "ipv4" | "ipv6";
|
||||
|
||||
@@ -32,6 +32,8 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _repairsIssues: RepairsIssue[] = [];
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _showIgnored = false;
|
||||
|
||||
private _getFilteredIssues = memoizeOne(
|
||||
@@ -75,7 +77,9 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config/system"
|
||||
}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.repairs.caption")}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user