Compare commits

..

7 Commits

Author SHA1 Message Date
Aidan Timson 5ce62ca5ec Prevent scrim closure on category dirty state 2026-06-02 13:50:39 +01:00
Aidan Timson 8a8b4bf138 Move dirty state provider to dialog build level using deferred state 2026-06-02 13:40:22 +01:00
Aidan Timson 6f6c860338 remove cast 2026-06-02 12:32:51 +01:00
Aidan Timson a79051caf1 Fix loop 2026-06-02 11:36:31 +01:00
Aidan Timson 2e459e8029 Deep state (existing) 2026-06-02 11:29:51 +01:00
Aidan Timson 373855ddb2 Shallow state (new) 2026-06-02 11:22:40 +01:00
Aidan Timson af8f250b60 Dirty state context provider 2026-06-02 11:19:33 +01:00
68 changed files with 784 additions and 932 deletions
-31
View File
@@ -13,28 +13,6 @@ export interface NetworkInfo {
supervisor_internet: boolean;
}
interface SupervisorJob {
name: string;
reference: string | null;
uuid: string;
progress: number; // float, 0100
stage: string | null;
done: boolean;
errors: {
type: string;
message: string;
stage: string | null;
}[];
created: string; // ISO datetime string
extra: Record<string, unknown> | null;
child_jobs: SupervisorJob[];
}
export interface SupervisorJobInfo {
ignore_conditions: string[];
jobs: SupervisorJob[];
}
export const ALTERNATIVE_DNS_SERVERS: {
ipv4: string[];
ipv6: string[];
@@ -79,15 +57,6 @@ export async function getSupervisorNetworkInfo(): Promise<NetworkInfo> {
return responseData?.data;
}
export async function getSupervisorJobsInfo(): Promise<
HassioResponse<SupervisorJobInfo>
> {
const responseData = await handleFetchPromise<
HassioResponse<SupervisorJobInfo>
>(fetch("/supervisor-api/jobs/info"));
return responseData;
}
export const setSupervisorNetworkDns = async (
dnsServerIndex: number,
primaryInterface: string
+6 -58
View File
@@ -2,9 +2,9 @@ import { mdiOpenInNew } from "@mdi/js";
import { css, html, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { extractSearchParam } from "../../src/common/url/search-params";
import "../../src/components/animation/ha-fade-in";
import "../../src/components/ha-alert";
import "../../src/components/ha-button";
import "../../src/components/animation/ha-fade-in";
import "../../src/components/ha-spinner";
import "../../src/components/ha-svg-icon";
import "../../src/components/progress/ha-progress-bar";
@@ -15,7 +15,6 @@ import { haStyle } from "../../src/resources/styles";
import "./components/landing-page-logs";
import "./components/landing-page-network";
import {
getSupervisorJobsInfo,
getSupervisorNetworkInfo,
pingSupervisor,
type NetworkInfo,
@@ -25,7 +24,6 @@ import { LandingPageBaseElement } from "./landing-page-base-element";
export const ASSUME_CORE_START_SECONDS = 60;
const SCHEDULE_CORE_CHECK_SECONDS = 1;
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
const SCHEDULE_FETCH_JOBS_INFO_SECONDS = 2;
@customElement("ha-landing-page")
class HaLandingPage extends LandingPageBaseElement {
@@ -41,8 +39,6 @@ class HaLandingPage extends LandingPageBaseElement {
@state() private _coreCheckActive = false;
@state() private _progress = -1;
private _mobileApp =
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
@@ -64,14 +60,7 @@ class HaLandingPage extends LandingPageBaseElement {
${!networkIssue && !this._supervisorError
? html`
<p>${this.localize("subheader")}</p>
<ha-progress-bar
.indeterminate=${this._progress <= 0}
.value=${this._progress > 0 ? this._progress : undefined}
.loading=${this._progress >= 0}
>${this._progress > 0
? `${Math.round(this._progress)}%`
: nothing}</ha-progress-bar
>
<ha-progress-bar indeterminate></ha-progress-bar>
`
: nothing}
${networkIssue || this._networkInfoError
@@ -137,7 +126,6 @@ class HaLandingPage extends LandingPageBaseElement {
import("../../src/components/ha-language-picker");
this._fetchSupervisorInfo(true);
this._fetchSupervisorJobsInfo();
}
private _scheduleFetchSupervisorInfo() {
@@ -150,13 +138,6 @@ class HaLandingPage extends LandingPageBaseElement {
);
}
private _scheduleFetchSupervisorJobsInfo() {
setTimeout(
() => this._fetchSupervisorJobsInfo(),
SCHEDULE_FETCH_JOBS_INFO_SECONDS * 1000
);
}
private _scheduleTurnOffCoreCheck() {
setTimeout(() => {
this._coreCheckActive = false;
@@ -184,7 +165,7 @@ class HaLandingPage extends LandingPageBaseElement {
// assume supervisor update if ping fails -> don't show an error
if (!this._coreCheckActive && err.message !== "ping-failed") {
// eslint-disable-next-line no-console
console.error("Failed to fetch supervisor info", err);
console.error(err);
this._networkInfoError = true;
}
}
@@ -194,33 +175,6 @@ class HaLandingPage extends LandingPageBaseElement {
}
}
private async _fetchSupervisorJobsInfo() {
try {
const jobsInfo = await getSupervisorJobsInfo();
const coreInstallJob =
jobsInfo.result === "ok"
? jobsInfo.data.jobs.find(
(job) => job.name === "home_assistant_core_install"
)
: undefined;
if (coreInstallJob) {
this._progress = coreInstallJob.progress;
} else {
this._progress = -1;
}
} catch (err: any) {
await this._checkCoreAvailability();
if (!this._coreCheckActive) {
this._progress = -1;
// eslint-disable-next-line no-console
console.error("Failed to fetch supervisor jobs info", err);
}
}
this._scheduleFetchSupervisorJobsInfo();
}
private async _checkCoreAvailability() {
try {
const response = await fetch("/manifest.json");
@@ -268,27 +222,21 @@ class HaLandingPage extends LandingPageBaseElement {
flex-direction: column;
gap: var(--ha-space-4);
}
ha-language-picker {
min-width: 200px;
}
ha-alert p {
text-align: unset;
}
.footer ha-svg-icon {
--mdc-icon-size: var(--ha-space-5);
}
ha-language-picker {
margin-inline-start: calc(-1 * var(--ha-space-4));
}
ha-button {
margin-inline-end: calc(-1 * var(--ha-space-2));
}
ha-fade-in {
min-height: calc(100vh - 64px - 88px);
display: flex;
justify-content: center;
align-items: center;
}
ha-progress-bar {
--ha-progress-bar-track-height: 20px;
}
`,
];
}
+4 -4
View File
@@ -70,8 +70,8 @@
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.23",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "4.1.1",
"@tsparticles/preset-links": "4.1.1",
"@tsparticles/engine": "4.1.0",
"@tsparticles/preset-links": "4.1.0",
"@vibrant/color": "4.0.4",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
@@ -88,7 +88,7 @@
"dialog-polyfill": "0.5.6",
"echarts": "6.1.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.4.0",
"fuse.js": "7.3.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
@@ -180,7 +180,7 @@
"jsdom": "29.1.1",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "5.0.1",
"lint-staged": "17.0.7",
"lint-staged": "17.0.5",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.18.1",
@@ -1,5 +1,6 @@
import { LitElement, css, html } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-tooltip";
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
@@ -12,6 +13,7 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
*
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
* @attr {string} label - Accessible label announced by assistive technology.
* @attr {string} message - Optional tooltip body shown on hover/focus.
*/
@customElement("ha-automation-row-live-test")
export class HaAutomationRowLiveTest extends LitElement {
@@ -19,6 +21,8 @@ export class HaAutomationRowLiveTest extends LitElement {
@property() public label = "";
@property() public message?: string;
protected render() {
return html`
<div
@@ -27,38 +31,39 @@ export class HaAutomationRowLiveTest extends LitElement {
tabindex="0"
aria-label=${this.label}
></div>
${this.message
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
: nothing}
`;
}
static styles = css`
:host {
position: absolute;
top: -5px;
inset-inline-end: -6px;
display: inline-block;
}
#indicator {
width: 10px;
height: 10px;
width: 12px;
height: 12px;
border-radius: var(--ha-border-radius-circle);
border: var(--ha-border-width-md) solid;
border: 3px solid;
box-sizing: border-box;
background-color: var(--card-background-color);
box-shadow: 0 0 0 2px var(--card-background-color);
transition: all var(--ha-animation-duration-normal) ease-in-out;
}
:host([state="pass"]) #indicator {
background-color: var(--ha-color-green-60);
border-color: var(--ha-color-green-60);
background-color: var(--ha-color-fill-success-loud-resting);
border-color: var(--ha-color-fill-success-loud-resting);
}
:host([state="fail"]) #indicator {
border-color: var(--ha-color-orange-60);
border-color: var(--ha-color-fill-warning-loud-resting);
}
:host([state="invalid"]) #indicator {
border-color: var(--ha-color-red-60);
border-color: var(--ha-color-fill-danger-loud-resting);
}
:host([state="unknown"]) #indicator {
border-color: var(--ha-color-neutral-60);
border-color: var(--ha-color-fill-neutral-loud-resting);
}
`;
}
@@ -165,8 +165,7 @@ export class HaAutomationRow extends LitElement {
::slotted([slot="leading-icon"]) {
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) ::slotted([slot="leading-icon"].action-icon),
:host([building-block]) ::slotted(#condition-icon) {
:host([building-block]) ::slotted([slot="leading-icon"]) {
--mdc-icon-size: var(--ha-space-5);
color: var(--white-color);
transform: rotate(-45deg);
+2 -6
View File
@@ -101,22 +101,18 @@ export class HaSankeyChart extends LitElement {
const value = this.valueFormatter
? this.valueFormatter(data.value)
: data.value;
// Keep numbers and units left-to-right, even in RTL locales.
const formattedValue = html`<div style="direction:ltr; display: inline;">
${value}
</div>`;
if (data.id) {
const node = this.data.nodes.find((n) => n.id === data.id);
return html`<ha-chart-tooltip-marker
.color=${String(params.color ?? "")}
></ha-chart-tooltip-marker>
${node?.label ?? data.id}<br />${formattedValue}`;
${node?.label ?? data.id}<br />${value}`;
}
if (data.source && data.target) {
const source = this.data.nodes.find((n) => n.id === data.source);
const target = this.data.nodes.find((n) => n.id === data.target);
return html`${source?.label ?? data.source}
${target?.label ?? data.target}<br />${formattedValue}`;
${target?.label ?? data.target}<br />${value}`;
}
return null;
};
+4 -7
View File
@@ -3,12 +3,13 @@ import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mainWindow } from "../common/dom/get_main_window";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
@customElement("ha-icon-button-arrow-prev")
export class HaIconButtonArrowPrev extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@@ -24,15 +25,11 @@ export class HaIconButtonArrowPrev extends LitElement {
@state() private _icon =
mainWindow.document.dir === "rtl" ? mdiArrowRight : mdiArrowLeft;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
protected render(): TemplateResult {
return html`
<ha-icon-button
.disabled=${this.disabled}
.label=${this.label || this._localize("ui.common.back") || "Back"}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
.path=${this._icon}
.href=${this.href}
.target=${this.target}
-11
View File
@@ -485,17 +485,6 @@ export const migrateAutomationTrigger = (
}
delete trigger.platform;
}
if ("options" in trigger) {
if (trigger.options && "behavior" in trigger.options) {
if (trigger.options.behavior === "any") {
trigger.options.behavior = "each";
} else if (trigger.options.behavior === "last") {
trigger.options.behavior = "all";
}
}
}
return trigger;
};
+21
View File
@@ -0,0 +1,21 @@
import { createContext } from "@lit/context";
export interface DirtyStateContext<State = unknown> {
/** Whether current state differs from the initial snapshot */
isDirty: boolean;
/** Current tracked state */
state: State;
/** Update the tracked state — triggers dirty comparison */
setState: (state: State) => void;
/** Reset initial snapshot to current state (marks clean) */
markClean: () => void;
}
/**
* Singleton context key for dirty-state tracking.
*
* Because Lit context keys are singletons, the value type is
* `DirtyStateContext<unknown>`. The provider mixin and consumer controller
* supply type-safe APIs on top of this boundary.
*/
export const dirtyStateContext = createContext<DirtyStateContext>("dirtyState");
+11 -3
View File
@@ -63,6 +63,7 @@ import { subscribeLabFeature } from "../../data/labs";
import type { ItemType } from "../../data/search";
import { SearchableDomains } from "../../data/search";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
import { ScrollableFadeMixin } from "../../mixins/scrollable-fade-mixin";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import {
@@ -121,8 +122,8 @@ declare global {
const DEFAULT_VIEW: MoreInfoView = "info";
@customElement("ha-more-info-dialog")
export class MoreInfoDialog extends SubscribeMixin(
ScrollableFadeMixin(LitElement)
export class MoreInfoDialog extends DirtyStateProviderMixin()(
SubscribeMixin(ScrollableFadeMixin(LitElement))
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -640,7 +641,8 @@ export class MoreInfoDialog extends SubscribeMixin(
@closed=${this._dialogClosed}
@opened=${this._handleOpened}
@show-child-view=${this._showChildView}
.preventScrimClose=${this._currView === "settings" ||
.preventScrimClose=${(this._currView === "settings" &&
this.isDirtyState) ||
!this._isEscapeEnabled}
flexcontent
>
@@ -957,6 +959,12 @@ export class MoreInfoDialog extends SubscribeMixin(
}
}
if (changedProps.has("_currView") || changedProps.has("_entry")) {
if (this._currView === "settings" && this._entry) {
this._initDirtyTracking({ type: "deep" });
}
}
if (changedProps.has("_currView")) {
this._infoEditMode = false;
this._detailsYamlMode = false;
+1
View File
@@ -33,6 +33,7 @@ class HassErrorScreen extends LitElement {
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
`}
+1
View File
@@ -35,6 +35,7 @@ class HassLoadingScreen extends LitElement {
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
`}
+2
View File
@@ -43,10 +43,12 @@ class HassSubpage extends LitElement {
? html`
<ha-icon-button-arrow-prev
href=${this.backPath}
.hass=${this.hass}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
+2 -2
View File
@@ -139,8 +139,6 @@ export class HassTabsSubpage extends LitElement {
);
public willUpdate(changedProperties: PropertyValues<this>) {
this.toggleAttribute("narrow", this._narrow);
if (changedProperties.has("route")) {
const currentPath = `${this.route.prefix}${this.route.path}`;
this._activeTab = this.tabs.find((tab) =>
@@ -175,10 +173,12 @@ export class HassTabsSubpage extends LitElement {
? html`
<ha-icon-button-arrow-prev
.href=${this.backPath}
.hass=${this.hass}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
+193
View File
@@ -0,0 +1,193 @@
import { provide } from "@lit/context";
import type { LitElement } from "lit";
import { state } from "lit/decorators";
import { deepEqual } from "../common/util/deep-equal";
import { shallowEqual } from "../common/util/shallow-equal";
import {
dirtyStateContext,
type DirtyStateContext,
} from "../data/context/dirty-state";
import type { Constructor } from "../types";
export type CompareStrategy<State> =
| { type: "deep" }
| { type: "shallow" }
| { type: "custom"; compare: (a: State, b: State) => boolean };
function resolveCompare<State>(
strategy: CompareStrategy<State>
): (a: State, b: State) => boolean {
switch (strategy.type) {
case "deep":
return (a, b) => deepEqual(a, b);
case "shallow":
return (a, b) => shallowEqual(a, b);
default:
return strategy.compare;
}
}
/**
* Mixin that provides dirty-state tracking via Lit context.
*
* Uses the `@provide` decorator so any descendant component can consume
* dirty-state with `@consume({ context: dirtyStateContext, subscribe: true })`.
*
* Curried generic pattern: `State` is explicitly provided while `Base` is
* inferred from the superclass argument.
*
* @example Eager init (state known upfront, e.g. dialog open):
* ```ts
* interface MyDialogState { name: string; icon: string }
*
* class MyDialog extends DirtyStateProviderMixin<MyDialogState>()(LitElement) {
* open() {
* this._initDirtyTracking({ type: "shallow" }, { name: "", icon: "" });
* }
* }
* ```
*
* @example Deferred init (child consumer reports initial state):
* ```ts
* class MyPage extends DirtyStateProviderMixin<FormState>()(LitElement) {
* connectedCallback() {
* super.connectedCallback();
* this._initDirtyTracking({ type: "deep" });
* // First setState from a child consumer sets the baseline
* }
* }
* ```
*
* Child consumers:
* ```ts
* @consume({ context: dirtyStateContext, subscribe: true })
* @state()
* private _dirtyState?: DirtyStateContext;
*
* // Read: this._dirtyState?.isDirty
* // Write: this._dirtyState?.setState(newState)
* ```
*/
export const DirtyStateProviderMixin =
<State = unknown>() =>
<Base extends Constructor<LitElement>>(superClass: Base) => {
class DirtyStateProviderMixinClass extends superClass {
private _dirtyInitialState: State | undefined;
private _dirtyCurrentState: State | undefined;
private _dirtyCompareFn: (a: State, b: State) => boolean = deepEqual;
@provide({ context: dirtyStateContext })
@state()
private _dirtyStateContext: DirtyStateContext = this._buildContextValue(
undefined,
false
);
/**
* Build the context value object for the provider.
*
* The returned type is `DirtyStateContext` (i.e. `DirtyStateContext<unknown>`)
* because the singleton context key is typed at `unknown`. The single
* `unknown → State` narrowing cast in `setState` is the only unsafe boundary
* and is confined here.
*/
private _buildContextValue(
currentState: State | undefined,
isDirty: boolean
): DirtyStateContext {
return {
isDirty,
state: currentState,
setState: (incoming: unknown) => {
this._updateDirtyState(incoming as State);
},
markClean: () => {
this._markDirtyStateClean();
},
};
}
/**
* Initialize dirty state tracking.
*
* When `initialState` is provided, tracking starts immediately.
* When omitted (deferred mode), the first `_updateDirtyState` /
* `setState` call from a consumer becomes the baseline snapshot.
*
* Call again to reset (e.g. when the underlying entity changes).
*/
protected _initDirtyTracking(
strategy: CompareStrategy<State>,
initialState?: State
): void {
this._dirtyCompareFn = resolveCompare(strategy);
if (initialState !== undefined) {
this._dirtyInitialState = initialState;
this._dirtyCurrentState = initialState;
this._dirtyStateContext = this._buildContextValue(
initialState,
false
);
} else {
this._dirtyInitialState = undefined;
this._dirtyCurrentState = undefined;
this._dirtyStateContext = this._buildContextValue(undefined, false);
}
}
/**
* Update the tracked state. Triggers dirty comparison against initial snapshot.
*
* If called before `_initDirtyTracking` provided an initial state (deferred
* mode), the first call sets the baseline and reports clean.
*
* Guarded: no-ops if the computed dirty status and state reference are
* unchanged, preventing render loops when called from `updated()`.
*/
protected _updateDirtyState(newState: State): void {
// Deferred init: first state becomes the baseline
if (this._dirtyInitialState === undefined) {
this._dirtyInitialState = newState;
this._dirtyCurrentState = newState;
this._dirtyStateContext = this._buildContextValue(newState, false);
return;
}
const isDirty = !this._dirtyCompareFn(
this._dirtyInitialState,
newState
);
if (
this._dirtyCurrentState !== undefined &&
this._dirtyCompareFn(this._dirtyCurrentState, newState) &&
this._dirtyStateContext.isDirty === isDirty
) {
return;
}
this._dirtyCurrentState = newState;
this._dirtyStateContext = this._buildContextValue(newState, isDirty);
}
/**
* Reset the initial snapshot to the current state, marking the state as clean.
* Call this after a successful save.
*/
protected _markDirtyStateClean(): void {
this._dirtyInitialState = this._dirtyCurrentState;
this._dirtyStateContext = this._buildContextValue(
this._dirtyCurrentState,
false
);
}
/**
* Whether the current state differs from the initial snapshot.
*/
public get isDirtyState(): boolean {
return this._dirtyStateContext.isDirty;
}
}
return DirtyStateProviderMixinClass;
};
+30 -19
View File
@@ -1,20 +1,39 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import type { LocalizeFunc } from "../../common/translations/localize";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import { DialogMixin } from "../../dialogs/dialog-mixin";
import type { AppDialogParams } from "./show-app-dialog";
@customElement("app-dialog")
class DialogApp extends DialogMixin<AppDialogParams>(LitElement) {
class DialogApp extends LitElement {
@property({ attribute: false }) public localize?: LocalizeFunc;
@state() private _open = false;
public async showDialog(params: { localize: LocalizeFunc }): Promise<void> {
this.localize = params.localize;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this.localize = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this.params?.localize) {
if (!this.localize) {
return nothing;
}
return html`<ha-dialog
open
header-title=${this.params.localize(
.open=${this._open}
header-title=${this.localize(
"ui.panel.page-onboarding.welcome.download_app"
) || "Click here to download the app"}
@closed=${this._dialogClosed}
>
<div>
<div class="app-qr">
@@ -26,17 +45,13 @@ class DialogApp extends DialogMixin<AppDialogParams>(LitElement) {
<img
loading="lazy"
src="/static/images/appstore.svg"
alt=${this.params.localize(
"ui.panel.page-onboarding.welcome.appstore"
)}
alt=${this.localize("ui.panel.page-onboarding.welcome.appstore")}
class="icon"
/>
<img
loading="lazy"
src="/static/images/qr-appstore.svg"
alt=${this.params.localize(
"ui.panel.page-onboarding.welcome.appstore"
)}
alt=${this.localize("ui.panel.page-onboarding.welcome.appstore")}
/>
</a>
<a
@@ -47,17 +62,13 @@ class DialogApp extends DialogMixin<AppDialogParams>(LitElement) {
<img
loading="lazy"
src="/static/images/playstore.svg"
alt=${this.params.localize(
"ui.panel.page-onboarding.welcome.playstore"
)}
alt=${this.localize("ui.panel.page-onboarding.welcome.playstore")}
class="icon"
/>
<img
loading="lazy"
src="/static/images/qr-playstore.svg"
alt=${this.params.localize(
"ui.panel.page-onboarding.welcome.playstore"
)}
alt=${this.localize("ui.panel.page-onboarding.welcome.playstore")}
/>
</a>
</div>
+75 -59
View File
@@ -1,87 +1,103 @@
import { mdiAccountGroup, mdiOpenInNew } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-dialog";
import "../../components/ha-svg-icon";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-nav";
import { DialogMixin } from "../../dialogs/dialog-mixin";
import type { CommunityDialogParams } from "./show-community-dialog";
import "../../components/ha-list";
import "../../components/ha-list-item";
@customElement("community-dialog")
class DialogCommunity extends DialogMixin<CommunityDialogParams>(LitElement) {
class DialogCommunity extends LitElement {
@property({ attribute: false }) public localize?: LocalizeFunc;
@state() private _open = false;
public async showDialog(params): Promise<void> {
this.localize = params.localize;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this.localize = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this.params?.localize) {
if (!this.localize) {
return nothing;
}
return html`<ha-dialog
open
header-title=${this.params.localize(
.open=${this._open}
header-title=${this.localize(
"ui.panel.page-onboarding.welcome.community"
)}
@closed=${this._dialogClosed}
>
<ha-list-nav>
<ha-list-item-button
<ha-list>
<a
target="_blank"
rel="noreferrer noopener"
href="https://community.home-assistant.io/"
>
<img
src="/static/icons/favicon-192x192.png"
slot="start"
alt="Home Assistant Logo"
/>
<span slot="headline">
${this.params.localize("ui.panel.page-onboarding.welcome.forums")}
</span>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item-button>
<ha-list-item-button
<ha-list-item hasMeta graphic="icon">
<img
src="/static/icons/favicon-192x192.png"
slot="graphic"
alt="Home Assistant Logo"
/>
${this.localize("ui.panel.page-onboarding.welcome.forums")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
<a
target="_blank"
rel="noreferrer noopener"
href="https://newsletter.openhomefoundation.org/"
>
<img
src="/static/icons/logo_ohf.svg"
slot="start"
alt="Open Home Foundation Logo"
/>
<span slot="headline">
${this.params.localize(
<ha-list-item hasMeta graphic="icon">
<img
src="/static/icons/logo_ohf.svg"
slot="graphic"
alt="Open Home Foundation Logo"
/>
${this.localize(
"ui.panel.page-onboarding.welcome.open_home_newsletter"
)}
</span>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item-button>
<ha-list-item-button
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
<a
target="_blank"
rel="noreferrer noopener"
href="https://www.home-assistant.io/join-chat"
>
<img
src="/static/images/logo_discord.png"
slot="start"
alt="Discord Logo"
/>
<span slot="headline">
${this.params.localize("ui.panel.page-onboarding.welcome.discord")}
</span>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item-button>
<ha-list-item-button
<ha-list-item hasMeta graphic="icon">
<img
src="/static/images/logo_discord.png"
slot="graphic"
alt="Discord Logo"
/>
${this.localize("ui.panel.page-onboarding.welcome.discord")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
<a
target="_blank"
rel="noreferrer noopener"
href="https://fosstodon.org/@homeassistant"
>
<ha-svg-icon .path=${mdiAccountGroup} slot="start"></ha-svg-icon>
<span slot="headline">
${this.params.localize(
"ui.panel.page-onboarding.welcome.social_media"
)}
</span>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item-button>
</ha-list-nav>
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon .path=${mdiAccountGroup} slot="graphic"></ha-svg-icon>
${this.localize("ui.panel.page-onboarding.welcome.social_media")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
</ha-list>
</ha-dialog>`;
}
@@ -89,12 +105,12 @@ class DialogCommunity extends DialogMixin<CommunityDialogParams>(LitElement) {
ha-dialog {
--dialog-content-padding: 0;
}
img {
width: 32px;
height: 32px;
ha-list-item {
height: 56px;
--mdc-list-item-meta-size: 20px;
}
ha-svg-icon {
color: var(--ha-color-text-secondary);
a {
text-decoration: none;
}
`;
}
+1 -6
View File
@@ -3,18 +3,13 @@ import type { LocalizeFunc } from "../../common/translations/localize";
export const loadAppDialog = () => import("./app-dialog");
export interface AppDialogParams {
localize: LocalizeFunc;
}
export const showAppDialog = (
element: HTMLElement,
params: AppDialogParams
params: { localize: LocalizeFunc }
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "app-dialog",
dialogImport: loadAppDialog,
dialogParams: params,
addHistory: false,
});
};
@@ -3,18 +3,13 @@ import type { LocalizeFunc } from "../../common/translations/localize";
export const loadCommunityDialog = () => import("./community-dialog");
export interface CommunityDialogParams {
localize: LocalizeFunc;
}
export const showCommunityDialog = (
element: HTMLElement,
params: CommunityDialogParams
params: { localize: LocalizeFunc }
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "community-dialog",
dialogImport: loadCommunityDialog,
dialogParams: params,
addHistory: false,
});
};
@@ -1,5 +1,5 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import { mdiCheckCircle, mdiHelpCircleOutline } from "@mdi/js";
import { mdiHelpCircleOutline } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
@@ -25,9 +25,7 @@ class SupervisorAppsCardContent extends LitElement {
@property() public stage: AddonStage = "stable";
@property() public state?: AddonState;
@property({ type: Boolean }) public installed = false;
@property() public state: AddonState = null;
@property() public description?: string;
@@ -79,23 +77,13 @@ class SupervisorAppsCardContent extends LitElement {
</div>
</div>
</div>
${this.tags?.length || this.state !== undefined || this.installed
${this.tags?.length || this.state
? html`
<div class="footer">
${this.state !== undefined
? html`<supervisor-apps-state
.state=${this.state || "unknown"}
></supervisor-apps-state>`
: this.installed
? html`<div class="installed">
<ha-svg-icon .path=${mdiCheckCircle}></ha-svg-icon>
<span
>${this.hass.localize(
"ui.panel.config.apps.state.installed"
)}</span
>
</div>`
: html`<span></span>`}
<supervisor-apps-state
.state=${this.state || "unknown"}
></supervisor-apps-state>
${this.tags?.length
? html`<div class="tags">
${this.tags.map(
@@ -171,17 +159,6 @@ class SupervisorAppsCardContent extends LitElement {
display: flex;
gap: var(--ha-space-2);
}
.installed {
display: inline-flex;
align-items: center;
gap: var(--ha-space-2);
color: var(--ha-color-text-secondary);
font-size: var(--ha-font-size-m);
}
.installed ha-svg-icon {
--mdc-icon-size: 16px;
color: var(--ha-color-on-success-normal);
}
`;
}
@@ -1,14 +1,7 @@
import {
mdiAlertDecagramOutline,
mdiArrowUpBoldCircle,
mdiArrowUpBoldCircleOutline,
mdiFlask,
mdiPuzzle,
} from "@mdi/js";
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
@@ -17,7 +10,6 @@ import type { HassioAddonRepository } from "../../../data/hassio/addon";
import type { StoreAddon } from "../../../data/supervisor/store";
import type { HomeAssistant } from "../../../types";
import "./components/supervisor-apps-card-content";
import type { AppTag } from "./components/supervisor-apps-card-content";
import { filterAndSort } from "./components/supervisor-apps-filter";
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
@@ -62,29 +54,21 @@ export class SupervisorAppsRepositoryEl extends LitElement {
<div class="content">
<h1>${repo.name}</h1>
<div class="card-group">
${addons.map((addon) => {
const tags = this._getAppTags(addon);
return html`
${addons.map(
(addon) => html`
<ha-card
outlined
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped}
>
<div
class=${classMap({
"card-content": true,
"has-footer": tags.length > 0 || addon.installed,
})}
>
<div class="card-content">
<supervisor-apps-card-content
.hass=${this.hass}
.title=${addon.name}
.stage=${addon.stage}
.description=${addon.description}
.available=${addon.available}
.installed=${addon.installed}
.tags=${tags}
.icon=${addon.installed && addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
@@ -124,8 +108,8 @@ export class SupervisorAppsRepositoryEl extends LitElement {
></supervisor-apps-card-content>
</div>
</ha-card>
`;
})}
`
)}
</div>
</div>
`;
@@ -135,32 +119,6 @@ export class SupervisorAppsRepositoryEl extends LitElement {
navigate(`/config/app/${ev.currentTarget.addon.slug}/info?store=true`);
}
private _getAppTags(addon: StoreAddon): AppTag[] {
const labels: AppTag[] = [];
if (addon.installed && addon.update_available) {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.state.update_available`
),
variant: "brand",
iconPath: mdiArrowUpBoldCircleOutline,
});
}
if (addon.stage !== "stable") {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
),
variant: addon.stage === "experimental" ? "warning" : "danger",
iconPath:
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
});
}
return labels;
}
static get styles(): CSSResultGroup {
return [
supervisorAppsStyle,
@@ -169,9 +127,6 @@ export class SupervisorAppsRepositoryEl extends LitElement {
cursor: pointer;
overflow: hidden;
}
.card-content.has-footer {
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
}
.not_available {
opacity: 0.6;
}
@@ -52,7 +52,6 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-tooltip";
import type {
AutomationClipboard,
Condition,
@@ -212,27 +211,11 @@ export default class HaAutomationConditionRow extends LitElement {
);
return html`
<div id="condition-icon" class="icon-badge-wrapper" slot="leading-icon">
<ha-condition-icon
.hass=${this.hass}
.condition=${this.condition.condition}
></ha-condition-icon>
${this.optionsInSidebar && this.condition.condition !== "trigger"
? html`<ha-automation-row-live-test
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
)}
></ha-automation-row-live-test>`
: nothing}
</div>
${this.optionsInSidebar &&
this.condition.condition !== "trigger" &&
this._liveTestResult.message
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
>${this._liveTestResult.message}</ha-tooltip
>`
: nothing}
<ha-condition-icon
slot="leading-icon"
.hass=${this.hass}
.condition=${this.condition.condition}
></ha-condition-icon>
<h3 slot="header">
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
@@ -548,7 +531,17 @@ export default class HaAutomationConditionRow extends LitElement {
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
>${this._renderRow()}
</ha-automation-row>`
<ha-automation-row-live-test
slot="icons"
.state=${this.condition.condition !== "trigger"
? this._liveTestResult.state
: "unknown"}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult.state : "unknown"}`
)}
.message=${this._liveTestResult.message}
></ha-automation-row-live-test
></ha-automation-row>`
: html`
<ha-expansion-panel
left-chevron
-5
View File
@@ -53,11 +53,6 @@ export const rowStyles = css`
position: absolute;
}
.icon-badge-wrapper {
position: relative;
display: inline-flex;
}
.note-indicator {
color: var(--ha-color-on-neutral-normal);
}
@@ -15,13 +15,19 @@ import type {
} from "../../../data/category_registry";
import { internationalizationContext } from "../../../data/context";
import { DialogMixin } from "../../../dialogs/dialog-mixin";
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import { haStyleDialog } from "../../../resources/styles";
import type { ValueChangedEvent } from "../../../types";
import type { CategoryRegistryDetailDialogParams } from "./show-dialog-category-registry-detail";
interface CategoryFormState {
name: string;
icon: string | null;
}
@customElement("dialog-category-registry-detail")
class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParams>(
LitElement
class DialogCategoryDetail extends DirtyStateProviderMixin<CategoryFormState>()(
DialogMixin<CategoryRegistryDetailDialogParams>(LitElement)
) {
@state()
@consume({ context: internationalizationContext, subscribe: true })
@@ -44,6 +50,10 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
this._name = this.params?.suggestedName || "";
this._icon = null;
}
this._initDirtyTracking(
{ type: "shallow" },
{ name: this._name, icon: this._icon }
);
}
protected render() {
@@ -52,13 +62,14 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
}
const entry = this.params.entry;
const nameInvalid = !this._isNameValid();
const isCreate = !entry;
return html`
<ha-dialog
open
header-title=${entry
? this._i18n.localize("ui.panel.config.category.editor.edit")
: this._i18n.localize("ui.panel.config.category.editor.create")}
prevent-scrim-close
.preventScrimClose=${this.isDirtyState}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
@@ -96,7 +107,9 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting}
.disabled=${nameInvalid ||
!!this._submitting ||
(!isCreate && !this.isDirtyState)}
>
${entry
? this._i18n.localize("ui.common.save")
@@ -114,15 +127,17 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
private _nameChanged(ev: InputEvent) {
this._error = undefined;
this._name = (ev.target as HaInput).value ?? "";
this._updateDirtyState({ name: this._name, icon: this._icon });
}
private _iconChanged(ev: ValueChangedEvent<string>) {
this._error = undefined;
this._icon = ev.detail.value;
this._updateDirtyState({ name: this._name, icon: this._icon });
}
private async _updateEntry() {
const create = !this.params!.entry;
const create = !this.params?.entry;
this._submitting = true;
let newValue: CategoryRegistryEntry | undefined;
try {
@@ -131,10 +146,11 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
icon: this._icon || (create ? undefined : null),
};
if (create) {
newValue = await this.params!.createEntry!(values);
newValue = await this.params?.createEntry?.(values);
} else {
newValue = await this.params!.updateEntry!(values);
newValue = await this.params?.updateEntry?.(values);
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._error =
@@ -36,6 +36,7 @@ class PanelDeveloperTools extends LitElement {
<div class="toolbar">
<ha-icon-button-arrow-prev
slot="navigationIcon"
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
<div class="main-title">
@@ -1,11 +1,13 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consume, type ContextType } from "@lit/context";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeEntityEntryName } from "../../../../../common/entity/compute_entity_name";
import "../../../../../components/ha-button";
import { dirtyStateContext } from "../../../../../data/context/dirty-state";
import type { ExtEntityRegistryEntry } from "../../../../../data/entity/entity_registry";
import { removeEntityRegistryEntry } from "../../../../../data/entity/entity_registry";
import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
@@ -33,14 +35,16 @@ export class EntitySettingsHelperTab extends LitElement {
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
@consume({ context: dirtyStateContext, subscribe: true })
@state()
private _dirtyState?: ContextType<typeof dirtyStateContext>;
@state() private _error?: string;
@state() private _item?: Helper | null;
@state() private _submitting = false;
@state() private _dirty = false;
@state() private _componentLoaded?: boolean;
@query("entity-registry-settings-editor")
@@ -60,13 +64,9 @@ export class EntitySettingsHelperTab extends LitElement {
super.updated(changedProperties);
if (changedProperties.has("entry")) {
this._error = undefined;
if (
this.entry.unique_id !==
(changedProperties.get("entry") as ExtEntityRegistryEntry)?.unique_id
) {
if (this.entry.unique_id !== changedProperties.get("entry")?.unique_id) {
this._item = undefined;
}
this._getItem();
}
}
@@ -107,7 +107,6 @@ export class EntitySettingsHelperTab extends LitElement {
.hass=${this.hass}
.entry=${this.entry}
.disabled=${!!this._submitting}
@change=${this._entityRegistryChanged}
hide-name
hide-icon
></entity-registry-settings-editor>
@@ -124,7 +123,7 @@ export class EntitySettingsHelperTab extends LitElement {
</ha-button>
<ha-button
@click=${this._updateItem}
.disabled=${!this._dirty ||
.disabled=${!(this._dirtyState?.isDirty || this._isHelperDirty) ||
!!this._submitting ||
!!(this._item && !this._item.name)}
>
@@ -139,22 +138,12 @@ export class EntitySettingsHelperTab extends LitElement {
return JSON.stringify(this._item) !== this._originalItemJson;
}
private _updateDirty() {
this._dirty = (this._registryEditor?.dirty ?? false) || this._isHelperDirty;
}
private _entityRegistryChanged() {
this._error = undefined;
this._updateDirty();
}
private _valueChanged(ev: CustomEvent): void {
if (this._item === null) {
return;
}
this._error = undefined;
this._item = ev.detail.value;
this._updateDirty();
}
private async _getItem() {
@@ -167,15 +156,20 @@ export class EntitySettingsHelperTab extends LitElement {
private async _updateItem(): Promise<void> {
this._submitting = true;
this._error = undefined;
try {
if (this._componentLoaded && this._item) {
await HELPERS_CRUD[this.entry.platform].update(
this.hass!,
this.hass,
this._item.id,
this._item
);
}
const result = await this._registryEditor!.updateEntry();
this._dirtyState?.markClean();
this._originalItemJson = this._item
? JSON.stringify(this._item)
: undefined;
if (result.close) {
fireEvent(this, "close-dialog");
}
@@ -6,8 +6,8 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { consume, type ContextType } from "@lit/context";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -45,6 +45,7 @@ import {
STREAM_TYPE_HLS,
updateCameraPrefs,
} from "../../../data/camera";
import { dirtyStateContext } from "../../../data/context/dirty-state";
import type { ConfigEntry } from "../../../data/config_entries";
import { deleteConfigEntry } from "../../../data/config_entries";
import {
@@ -144,6 +145,28 @@ const SCANNER_SOURCE_TYPES = ["router", "bluetooth", "bluetooth_le"];
const ZONE_DOMAINS = ["zone"];
interface EntitySettingsState {
name: string | null;
icon: string | null;
entityId: string;
areaId: string | null;
labels: string[];
deviceClass: string | undefined;
disabledBy: EntityRegistryEntry["disabled_by"];
hiddenBy: EntityRegistryEntry["hidden_by"];
unitOfMeasurement: string | null | undefined;
precision: number | null | undefined;
defaultCode: string | null | undefined;
calendarColor: string | null;
precipitationUnit: string | null | undefined;
pressureUnit: string | null | undefined;
temperatureUnit: string | null | undefined;
visibilityUnit: string | null | undefined;
windSpeedUnit: string | null | undefined;
switchAsDomain: string;
switchAsInvert: boolean;
}
@customElement("entity-registry-settings-editor")
export class EntityRegistrySettingsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -158,41 +181,50 @@ export class EntityRegistrySettingsEditor extends LitElement {
@property({ attribute: false }) public helperConfigEntry?: ConfigEntry;
@consume({ context: dirtyStateContext, subscribe: true })
@state()
private _dirtyState?: ContextType<typeof dirtyStateContext>;
@state() private _name!: string;
@state() private _icon!: string;
@state() private _entityId!: string;
@state() private _entityId!: EntitySettingsState["entityId"];
@state() private _deviceClass?: string;
@state() private _deviceClass?: EntitySettingsState["deviceClass"];
@state() private _switchAsDomain = "switch";
@state() private _switchAsDomain: EntitySettingsState["switchAsDomain"] =
"switch";
@state() private _switchAsInvert = false;
@state() private _switchAsInvert: EntitySettingsState["switchAsInvert"] =
false;
@state() private _areaId?: string | null;
@state() private _labels?: string[] | null;
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
@state() private _disabledBy!: EntitySettingsState["disabledBy"];
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
@state() private _hiddenBy!: EntitySettingsState["hiddenBy"];
@state() private _device?: DeviceRegistryEntry;
@state() private _unit_of_measurement?: string | null;
@state()
private _unit_of_measurement?: EntitySettingsState["unitOfMeasurement"];
@state() private _precision?: number | null;
@state() private _precision?: EntitySettingsState["precision"];
@state() private _precipitation_unit?: string | null;
@state()
private _precipitation_unit?: EntitySettingsState["precipitationUnit"];
@state() private _pressure_unit?: string | null;
@state() private _pressure_unit?: EntitySettingsState["pressureUnit"];
@state() private _temperature_unit?: string | null;
@state()
private _temperature_unit?: EntitySettingsState["temperatureUnit"];
@state() private _visibility_unit?: string | null;
@state() private _visibility_unit?: EntitySettingsState["visibilityUnit"];
@state() private _wind_speed_unit?: string | null;
@state() private _wind_speed_unit?: EntitySettingsState["windSpeedUnit"];
@state() private _cameraPrefs?: CameraPreferences;
@@ -204,9 +236,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
@state() private _weatherConvertibleUnits?: WeatherUnits;
@state() private _defaultCode?: string | null;
@state() private _defaultCode?: EntitySettingsState["defaultCode"];
@state() private _calendarColor?: string | null;
@state() private _calendarColor?: EntitySettingsState["calendarColor"];
@state() private _associatedZone?: string;
@@ -216,11 +248,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
private _deviceClassOptions?: string[][];
private _initialStateJson!: string;
private _lastDirty = false;
private _currentState() {
private _currentState(): EntitySettingsState {
return {
name: this._name.trim() || null,
icon: this._icon.trim() || null,
@@ -315,9 +343,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._wind_speed_unit = stateObj?.attributes?.wind_speed_unit;
}
this._initialStateJson = JSON.stringify(this._currentState());
this._lastDirty = false;
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
if (!deviceClasses || this._hideDeviceClassOverride(domain)) {
@@ -416,17 +441,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._switchAsDomain = "switch";
this._switchAsInvert = false;
}
this._initialStateJson = JSON.stringify(this._currentState());
this._lastDirty = false;
}
if (this._initialStateJson) {
const dirty = this.dirty;
if (dirty !== this._lastDirty) {
this._lastDirty = dirty;
fireEvent(this, "change");
}
}
this._dirtyState?.setState(this._currentState());
}
protected render() {
@@ -1146,10 +1163,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
`;
}
public get dirty(): boolean {
return JSON.stringify(this._currentState()) !== this._initialStateJson;
}
public async updateEntry(): Promise<{
close: boolean;
entry: ExtEntityRegistryEntry;
@@ -1435,12 +1448,10 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
private _nameChanged(ev: InputEvent): void {
fireEvent(this, "change");
this._name = (ev.target as HTMLInputElement).value;
}
private _iconChanged(ev: CustomEvent): void {
fireEvent(this, "change");
this._icon = ev.detail.value;
}
@@ -1459,22 +1470,18 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
private _entityIdChanged(ev: InputEvent): void {
fireEvent(this, "change");
this._entityId = `${computeDomain(this._origEntityId)}.${(ev.target as HTMLInputElement).value}`;
}
private _deviceClassChanged(ev: HaSelectSelectEvent<string, true>): void {
fireEvent(this, "change");
this._deviceClass = ev.detail.value;
}
private _unitChanged(ev: HaSelectSelectEvent): void {
fireEvent(this, "change");
this._unit_of_measurement = ev.detail.value;
}
private _defaultcodeChanged(ev: InputEvent): void {
fireEvent(this, "change");
this._defaultCode =
(ev.target as HTMLInputElement).value === ""
? null
@@ -1482,43 +1489,35 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
private _calendarColorChanged(ev: CustomEvent): void {
fireEvent(this, "change");
this._calendarColor = ev.detail.value || null;
}
private _associatedZoneChanged(ev: CustomEvent): void {
fireEvent(this, "change");
this._associatedZone = ev.detail.value || "zone.home";
}
private _precipitationUnitChanged(ev: HaSelectSelectEvent): void {
fireEvent(this, "change");
this._precipitation_unit = ev.detail.value;
}
private _precisionChanged(ev: HaSelectSelectEvent): void {
fireEvent(this, "change");
this._precision =
ev.detail.value === "default" ? null : Number(ev.detail.value);
}
private _pressureUnitChanged(ev: HaSelectSelectEvent): void {
fireEvent(this, "change");
this._pressure_unit = ev.detail.value;
}
private _temperatureUnitChanged(ev: HaSelectSelectEvent): void {
fireEvent(this, "change");
this._temperature_unit = ev.detail.value;
}
private _visibilityUnitChanged(ev: HaSelectSelectEvent): void {
fireEvent(this, "change");
this._visibility_unit = ev.detail.value;
}
private _windSpeedUnitChanged(ev: HaSelectSelectEvent): void {
fireEvent(this, "change");
this._wind_speed_unit = ev.detail.value;
}
@@ -1553,7 +1552,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
private _areaPicked(ev: CustomEvent) {
fireEvent(this, "change");
this._areaId = ev.detail.value;
}
@@ -1626,8 +1624,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
private _resetNameAndOpenDeviceSettings() {
this._name = this.entry.name || "";
fireEvent(this, "change");
this._openDeviceSettings();
}
@@ -2,6 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consume, type ContextType } from "@lit/context";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDeviceName } from "../../../common/entity/compute_device_name";
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
@@ -13,6 +14,7 @@ import {
deleteConfigEntry,
getConfigEntry,
} from "../../../data/config_entries";
import { dirtyStateContext } from "../../../data/context/dirty-state";
import { updateDeviceRegistryEntry } from "../../../data/device/device_registry";
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
import {
@@ -38,14 +40,16 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ type: Object }) public entry!: ExtEntityRegistryEntry;
@consume({ context: dirtyStateContext, subscribe: true })
@state()
private _dirtyState?: ContextType<typeof dirtyStateContext>;
@state() private _helperConfigEntry?: ConfigEntry;
@state() private _error?: string;
@state() private _submitting?: boolean;
@state() private _dirty = false;
@query("entity-registry-settings-editor")
private _registryEditor?: EntityRegistrySettingsEditor;
@@ -133,7 +137,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
.entry=${this.entry}
.helperConfigEntry=${this._helperConfigEntry}
.disabled=${!!this._submitting}
@change=${this._entityRegistryChanged}
></entity-registry-settings-editor>
</div>
<div class="buttons">
@@ -148,7 +151,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
</ha-button>
<ha-button
@click=${this._updateEntry}
.disabled=${!this._dirty || !!this._submitting}
.disabled=${!this._dirtyState?.isDirty || !!this._submitting}
.loading=${!!this._submitting}
>
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
@@ -157,11 +160,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
`;
}
private _entityRegistryChanged() {
this._error = undefined;
this._dirty = this._registryEditor?.dirty ?? false;
}
private _openDeviceSettings() {
const device = this.hass.devices[this.entry.device_id!];
@@ -207,8 +205,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
private async _updateEntry(): Promise<void> {
this._submitting = true;
this._error = undefined;
try {
const result = await this._registryEditor!.updateEntry();
this._dirtyState?.markClean();
if (result.close) {
fireEvent(this, "close-dialog");
}
@@ -25,7 +25,6 @@ import {
type ExtEntityRegistryEntry,
} from "../../../../../data/entity/entity_registry";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { OVERRIDE_DEVICE_CLASSES } from "../../../entities/entity-registry-settings-editor";
import "./matter-add-device/matter-add-device-apple-home";
import "./matter-add-device/matter-add-device-existing";
import "./matter-add-device/matter-add-device-generic";
@@ -140,17 +139,15 @@ class DialogMatterAddDevice extends LitElement {
entityIds
);
this._mainEntity = Object.values(entries).find((entry) => {
if (entry.entity_category) return false;
const domain = computeDomain(entry.entity_id);
const deviceClasses = OVERRIDE_DEVICE_CLASSES[domain];
if (!deviceClasses) return false;
const deviceClass = entry.device_class ?? entry.original_device_class;
if (!deviceClass) return false;
return deviceClasses.some(
(classes) => classes.length > 1 && classes.includes(deviceClass)
);
});
const mainEntry = Object.values(entries).find(
(e) => e.original_name === null
);
if (!mainEntry) return;
const domain = computeDomain(mainEntry.entity_id);
if (domain === "cover" || domain === "binary_sensor") {
this._mainEntity = mainEntry;
}
}
private _dialogClosed(): void {
@@ -375,6 +372,7 @@ class DialogMatterAddDevice extends LitElement {
? html`
<ha-icon-button-arrow-prev
slot="headerNavigationIcon"
.hass=${this.hass}
@click=${this._back}
></ha-icon-button-arrow-prev>
`
@@ -10,7 +10,6 @@ import type { LovelaceConfig } from "../../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LocalizeKeys } from "../../../common/translations/localize";
import type { HomeAssistant } from "../../../types";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import {
DEFAULT_ENERGY_COLLECTION_KEY,
DEFAULT_POWER_COLLECTION_KEY,
@@ -69,8 +68,6 @@ export interface EnergyDashboardStrategyConfig extends LovelaceStrategyConfig {
@customElement("energy-dashboard-strategy")
export class EnergyDashboardStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: EnergyDashboardStrategyConfig,
hass: HomeAssistant
@@ -5,13 +5,10 @@ import { getEnergyDataCollection } from "../../../data/energy";
import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
@customElement("energy-overview-view-strategy")
export class EnergyOverviewViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
@@ -12,12 +12,9 @@ import {
LARGE_SCREEN_CONDITION,
SMALL_SCREEN_CONDITION,
} from "../../lovelace/strategies/helpers/view-columns-conditions";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
@customElement("energy-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
@@ -6,12 +6,9 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
@customElement("gas-view-strategy")
export class GasViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
@@ -8,12 +8,9 @@ import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
@customElement("power-view-strategy")
export class PowerViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
@@ -5,14 +5,11 @@ import type { LovelaceSectionConfig } from "../../../data/lovelace/config/sectio
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
@customElement("water-view-strategy")
export class WaterViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
+40 -44
View File
@@ -7,6 +7,7 @@ import { styleMap } from "lit/directives/style-map";
import { atLeastVersion } from "../../common/config/version";
import { navigate } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-button";
import "../../components/ha-svg-icon";
import { updateAreaRegistryEntry } from "../../data/area/area_registry";
@@ -25,10 +26,7 @@ import { showDeviceRegistryDetailDialog } from "../config/devices/device-registr
import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog";
import "../lovelace/hui-root";
import type { ExtraActionItem } from "../lovelace/hui-root";
import {
checkStrategyShouldRegenerate,
generateLovelaceDashboardStrategy,
} from "../lovelace/strategies/get-strategy";
import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
import { showNewOverviewDialog } from "./dialogs/show-dialog-new-overview";
@@ -95,37 +93,33 @@ class PanelHome extends LitElement {
return;
}
const oldHass = changedProps.get("hass") as this["hass"] | undefined;
if (!oldHass) {
return;
}
// Locale changed: regenerate to refresh translated content
if (oldHass.localize !== this.hass.localize) {
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
this._setLovelace();
return;
}
if (this.hass.config.state !== "RUNNING") {
return;
}
// Home Assistant just started: run the full setup
if (oldHass.config.state !== "RUNNING") {
this._setup();
return;
}
// A registry the strategy depends on changed: regenerate
if (
checkStrategyShouldRegenerate(
"dashboard",
this._strategyConfig.strategy,
oldHass,
this.hass
)
) {
this._debounceRegenerateStrategy();
if (oldHass && this.hass) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors ||
oldHass.panels !== this.hass.panels
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setup();
}
}
}
@@ -150,12 +144,12 @@ class PanelHome extends LitElement {
}
}
private _debounceRegenerateStrategy = debounce(
() => this._regenerateStrategyConfig(),
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _regenerateStrategyConfig() {
private _registriesChanged = async () => {
// If on an area view that no longer exists, redirect to overview
const path = this.route?.path?.split("/")[1];
if (path?.startsWith("areas-")) {
@@ -166,7 +160,7 @@ class PanelHome extends LitElement {
}
}
this._setLovelace();
}
};
private _updateExtraActionItems() {
const path = this.route?.path?.split("/")[1];
@@ -326,8 +320,11 @@ class PanelHome extends LitElement {
});
}
private get _strategyConfig(): LovelaceDashboardStrategyConfig {
return {
private async _setLovelace() {
if (this._loadConfigPromise) {
await this._loadConfigPromise;
}
const strategyConfig: LovelaceDashboardStrategyConfig = {
strategy: {
type: "home",
favorite_entities: this._config.favorite_entities,
@@ -337,17 +334,16 @@ class PanelHome extends LitElement {
shortcuts: this._config.shortcuts,
},
};
}
private async _setLovelace() {
if (this._loadConfigPromise) {
await this._loadConfigPromise;
}
const config = await generateLovelaceDashboardStrategy(
this._strategyConfig,
const config = await expandLovelaceConfigStrategies(
strategyConfig,
this.hass
);
if (deepEqual(config, this._lovelace?.config)) {
return;
}
this._lovelace = {
config: config,
rawConfig: config,
@@ -438,7 +438,9 @@ class HuiEnergySankeyCard
}
private _valueFormatter = (value: number) =>
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
`<div style="direction:ltr; display: inline;">
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
kWh</div>`;
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
const { node } = ev.detail;
@@ -580,7 +580,9 @@ class HuiPowerSankeyCard
}
private _valueFormatter = (value: number) =>
formatPowerShort(this.hass, value);
`<div style="direction:ltr; display: inline;">
${formatPowerShort(this.hass, value)}
</div>`;
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
const { node } = ev.detail;
@@ -511,11 +511,9 @@ class HuiWaterFlowSankeyCard
}
private _valueFormatter = (value: number) =>
formatFlowRateShort(
this.hass.locale,
this.hass.config.unit_system.length,
value
);
`<div style="direction:ltr; display: inline;">
${formatFlowRateShort(this.hass.locale, this.hass.config.unit_system.length, value)}
</div>`;
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
const { node } = ev.detail;
@@ -2,7 +2,7 @@ import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
@@ -66,19 +66,10 @@ export class HuiBadgePicker extends LitElement {
@state() private _height?: number;
@query("ha-input-search") private _searchInput?: HaInputSearch;
private _unusedEntities?: string[];
private _usedEntities?: string[];
public async focus(): Promise<void> {
await this.updateComplete;
// Wait for the input's inner wa-input to render so focus delegation works.
await this._searchInput?.updateComplete;
this._searchInput?.focus();
}
private _filterBadges = memoizeOne(
(badgeElements: BadgeElement[], filter?: string): BadgeElement[] => {
if (!filter) {
@@ -62,17 +62,19 @@ export class HuiCardPicker extends LitElement {
@state() private _filter = "";
@query("ha-input-search") private _searchInput?: HaInputSearch;
@query("ha-input-search") private _searchInput?: HTMLElement;
private _unusedEntities?: string[];
private _usedEntities?: string[];
public async focus(): Promise<void> {
await this.updateComplete;
// Wait for the input's inner wa-input to render so focus delegation works.
await this._searchInput?.updateComplete;
this._searchInput?.focus();
if (this._searchInput) {
this._searchInput.focus();
} else {
await this.updateComplete;
this.focus();
}
}
private _filterCards = memoizeOne(
@@ -133,7 +133,6 @@ export class HuiCreateDialogCard
this._currTab === "entity"
? html`
<hui-suggestion-picker
?autofocus=${!this._narrow}
.hass=${this.hass}
.prioritizedCardTypes=${this._params.suggestedCards}
@suggestion-picked=${this._handleSuggestionPicked}
@@ -8,7 +8,7 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { transform } from "../../../../common/decorators/transform";
@@ -85,15 +85,6 @@ export class HuiSuggestionEntityTree extends LitElement {
@state() private _fuseIndex?: EntityFuseIndex;
@query("ha-input-search") private _searchInput?: HaInputSearch;
public async focus(): Promise<void> {
await this.updateComplete;
// Wait for the input's inner wa-input to render so focus delegation works.
await this._searchInput?.updateComplete;
this._searchInput?.focus();
}
public connectedCallback(): void {
super.connectedCallback();
this._loadDomainTranslations();
@@ -144,7 +135,7 @@ export class HuiSuggestionEntityTree extends LitElement {
}
protected render() {
if (!this.hass) return nothing;
if (!this.hass || !this._tree) return nothing;
return html`
<ha-input-search
@@ -155,13 +146,11 @@ export class HuiSuggestionEntityTree extends LitElement {
)}
@input=${this._handleFilterChange}
></ha-input-search>
${this._tree
? this._filter
? this._renderSearchResults()
: html`<div class="tree ha-scrollbar">
${this._renderTree(this._tree)}
</div>`
: nothing}
${this._filter
? this._renderSearchResults()
: html`<div class="tree ha-scrollbar">
${this._renderTree(this._tree)}
</div>`}
`;
}
@@ -1,7 +1,7 @@
import { mdiClose, mdiViewGridPlus } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
@@ -24,7 +24,6 @@ import {
import type { CardSuggestion } from "../../card-suggestions/types";
import "./hui-suggestion-card";
import "./hui-suggestion-entity-tree";
import type { HuiSuggestionEntityTree } from "./hui-suggestion-entity-tree";
@customElement("hui-suggestion-picker")
export class HuiSuggestionPicker extends LitElement {
@@ -39,14 +38,6 @@ export class HuiSuggestionPicker extends LitElement {
private _narrowMql?: MediaQueryList;
@query("hui-suggestion-entity-tree")
private _entityTree?: HuiSuggestionEntityTree;
public async focus(): Promise<void> {
await this.updateComplete;
await this._entityTree?.focus();
}
public connectedCallback(): void {
super.connectedCallback();
this._narrowMql = matchMedia("(max-width: 600px)");
@@ -30,7 +30,6 @@ import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import "../../../../components/ha-yaml-editor";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
@@ -231,28 +230,11 @@ export class HaCardConditionEditor extends LitElement {
return html`
<div class="container">
<ha-expansion-panel left-chevron>
<div
id="condition-icon"
class="icon-badge-wrapper"
<ha-svg-icon
slot="leading-icon"
>
<ha-svg-icon
.path=${ICON_CONDITION[condition.condition]}
></ha-svg-icon>
${hideLiveTest
? nothing
: html`<ha-automation-row-live-test
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
)}
></ha-automation-row-live-test>`}
</div>
${!hideLiveTest && this._liveTestResult.message
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
>${this._liveTestResult.message}</ha-tooltip
>`
: nothing}
class="condition-icon"
.path=${ICON_CONDITION[condition.condition]}
></ha-svg-icon>
<h3 slot="header">
${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
@@ -273,6 +255,18 @@ export class HaCardConditionEditor extends LitElement {
"ui.panel.lovelace.editor.condition-editor.testing_error"
)}
</ha-automation-row-event-chip>
${hideLiveTest
? nothing
: html`
<ha-automation-row-live-test
slot="icons"
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
)}
.message=${this._liveTestResult.message}
></ha-automation-row-live-test>
`}
<ha-dropdown
slot="icons"
@wa-select=${this._handleAction}
@@ -485,15 +479,17 @@ export class HaCardConditionEditor extends LitElement {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
.icon-badge-wrapper {
.condition-icon {
display: none;
}
@media (min-width: 870px) {
.icon-badge-wrapper {
display: inline-flex;
position: relative;
.condition-icon {
display: inline-block;
color: var(--secondary-text-color);
opacity: 0.9;
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
}
}
h3 {
+54 -14
View File
@@ -12,6 +12,7 @@ import {
removeSearchParam,
} from "../../common/url/search-params";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-button";
import { domainToName } from "../../data/integration";
import { subscribeLovelaceUpdates } from "../../data/lovelace";
@@ -35,10 +36,7 @@ import { checkLovelaceConfig } from "./common/check-lovelace-config";
import { loadLovelaceResources } from "./common/load-resources";
import { showSaveDialog } from "./editor/show-save-config-dialog";
import "./hui-root";
import {
checkStrategyShouldRegenerate,
generateLovelaceDashboardStrategy,
} from "./strategies/get-strategy";
import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy";
import type { Lovelace } from "./types";
import { generateDefaultView } from "./views/default-view";
import { fetchDashboards } from "../../data/lovelace/dashboard";
@@ -52,6 +50,12 @@ interface LovelacePanelConfig {
let editorLoaded = false;
let resourcesLoaded = false;
declare global {
interface HASSDomEvents {
"strategy-config-changed": undefined;
}
}
@customElement("ha-panel-lovelace")
export class LovelacePanel extends LitElement {
@property({ attribute: false }) public panel?: PanelInfo<
@@ -125,6 +129,7 @@ export class LovelacePanel extends LitElement {
.route=${this.route}
.narrow=${this.narrow}
@config-refresh=${this._forceFetchConfig}
@strategy-config-changed=${this._strategyConfigChanged}
></hui-root>
`;
}
@@ -190,26 +195,61 @@ export class LovelacePanel extends LitElement {
this.lovelace &&
isStrategyDashboard(this.lovelace.rawConfig)
) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
(oldHass.config.state !== "RUNNING" ||
checkStrategyShouldRegenerate(
"dashboard",
this.lovelace.rawConfig.strategy,
oldHass,
this.hass
))
oldHass.config.state !== "RUNNING"
) {
this._debounceRegenerateStrategy();
this._regenerateStrategyConfig();
}
}
}
private _debounceRegenerateStrategy = debounce(
() => this._regenerateStrategyConfig(),
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _registriesChanged = async () => {
if (!this.hass || !this.lovelace) {
return;
}
const rawConfig = this.lovelace.rawConfig;
if (!isStrategyDashboard(rawConfig)) {
return;
}
const oldConfig = this.lovelace.config;
const generatedConfig = await generateLovelaceDashboardStrategy(
rawConfig,
this.hass!
);
const newConfig = checkLovelaceConfig(generatedConfig) as LovelaceConfig;
// Regenerate if the config changed
if (!deepEqual(newConfig, oldConfig)) {
this._regenerateStrategyConfig();
}
};
private _strategyConfigChanged = (ev: CustomEvent) => {
ev.stopPropagation();
this._regenerateStrategyConfig();
};
private async _regenerateStrategyConfig() {
if (!this.hass || !this.lovelace) {
return;
+2
View File
@@ -488,6 +488,7 @@ class HUIRoot extends LitElement {
${this._editMode
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.move_left"
)}
@@ -571,6 +572,7 @@ class HUIRoot extends LitElement {
${isSubview || this.backButton
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
+1 -32
View File
@@ -6,7 +6,6 @@ import { storage } from "../../../common/decorators/storage";
import { deepEqual } from "../../../common/util/deep-equal";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-svg-icon";
import type { LovelaceSectionElement } from "../../../data/lovelace";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@@ -26,10 +25,7 @@ import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"
import { addCard, replaceCard } from "../editor/config-util";
import { performDeleteCard } from "../editor/delete-card";
import { parseLovelaceCardPath } from "../editor/lovelace-path";
import {
checkStrategyShouldRegenerate,
generateLovelaceSectionStrategy,
} from "../strategies/get-strategy";
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
import type { Lovelace } from "../types";
import { DEFAULT_SECTION_LAYOUT } from "./const";
@@ -110,36 +106,9 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
(!oldConfig || this.config !== oldConfig)
) {
this._initializeConfig();
return;
}
if (!changedProperties.has("hass")) {
return;
}
const oldHass = changedProperties.get("hass") as HomeAssistant | undefined;
if (
oldHass &&
this.hass &&
isStrategySection(this.config) &&
this.hass.config.state === "RUNNING" &&
(oldHass.config.state !== "RUNNING" ||
checkStrategyShouldRegenerate(
"section",
this.config.strategy,
oldHass,
this.hass
))
) {
this._debounceRefreshConfig();
}
}
private _debounceRefreshConfig = debounce(
() => this._initializeConfig(),
200
);
public disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener(
@@ -6,7 +6,6 @@ import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceStrategyDependency } from "../types";
import {
AREA_STRATEGY_GROUP_ICONS,
computeAreaTileCardConfig,
@@ -35,12 +34,6 @@ const computeHeadingCard = (
@customElement("area-view-strategy")
export class AreaViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [
"entities",
"devices",
"areas",
];
static async generate(
config: AreaViewStrategyConfig,
hass: HomeAssistant
@@ -4,10 +4,7 @@ import { customElement } from "lit/decorators";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type {
LovelaceStrategyEditor,
LovelaceStrategyDependency,
} from "../types";
import type { LovelaceStrategyEditor } from "../types";
import type {
AreaViewStrategyConfig,
EntitiesDisplay,
@@ -34,10 +31,6 @@ export interface AreasDashboardStrategyConfig {
@customElement("areas-dashboard-strategy")
export class AreasDashboardStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [
"areas",
];
static async generate(
config: AreasDashboardStrategyConfig,
hass: HomeAssistant
@@ -10,7 +10,6 @@ import {
type AreaControlDomain,
} from "../../card-features/types";
import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
import type { LovelaceStrategyDependency } from "../types";
import type { EntitiesDisplay } from "./area-view-strategy";
import {
computeAreaPath,
@@ -39,13 +38,6 @@ export interface AreasViewStrategyConfig {
@customElement("areas-overview-view-strategy")
export class AreasOverviewViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [
"entities",
"devices",
"areas",
"floors",
];
static async generate(
config: AreasViewStrategyConfig,
hass: HomeAssistant
+10 -76
View File
@@ -23,20 +23,12 @@ import type {
LovelaceDashboardStrategyGetCreateSuggestions,
LovelaceSectionStrategy,
LovelaceStrategy,
LovelaceStrategyDependency,
LovelaceViewStrategy,
} from "./types";
const MAX_WAIT_STRATEGY_LOAD = 5000;
const CUSTOM_PREFIX = "custom:";
const DEFAULT_REGISTRY_DEPENDENCIES: readonly LovelaceStrategyDependency[] = [
"entities",
"devices",
"areas",
"floors",
];
const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
dashboard: {
"original-states": () =>
@@ -90,47 +82,24 @@ type StrategyConfig<T extends LovelaceStrategyConfigType> = AsyncReturnType<
Strategies[T]["generate"]
>;
type StrategyTag =
| { type: "builtin"; tag: string }
| { type: "custom"; tag: string; legacyTag: string };
// Resolves the custom element tag(s) for a strategy. Custom strategies also
// expose a legacy tag. `undefined` means the type is neither built-in nor a
// custom strategy.
const getStrategyTag = (
configType: LovelaceStrategyConfigType,
strategyType: string
): StrategyTag | undefined => {
if (strategyType in STRATEGIES[configType]) {
return { type: "builtin", tag: `${strategyType}-${configType}-strategy` };
}
if (strategyType.startsWith(CUSTOM_PREFIX)) {
const name = strategyType.slice(CUSTOM_PREFIX.length);
return {
type: "custom",
tag: `ll-strategy-${configType}-${name}`,
legacyTag: `ll-strategy-${name}`,
};
}
return undefined;
};
export const getLovelaceStrategy = async <T extends LovelaceStrategyConfigType>(
configType: T,
strategyType: string
): Promise<LovelaceStrategy> => {
const tags = getStrategyTag(configType, strategyType);
if (strategyType in STRATEGIES[configType]) {
await STRATEGIES[configType][strategyType]();
const tag = `${strategyType}-${configType}-strategy`;
return customElements.get(tag) as unknown as Strategies[T];
}
if (!tags) {
if (!strategyType.startsWith(CUSTOM_PREFIX)) {
throw new Error("Unknown strategy");
}
if (tags.type === "builtin") {
await STRATEGIES[configType][strategyType]();
return customElements.get(tags.tag) as unknown as Strategies[T];
}
const { tag, legacyTag } = tags;
const legacyTag = `ll-strategy-${strategyType.slice(CUSTOM_PREFIX.length)}`;
const tag = `ll-strategy-${configType}-${strategyType.slice(
CUSTOM_PREFIX.length
)}`;
if (
(await Promise.race([
@@ -272,41 +241,6 @@ export const generateLovelaceSectionStrategy = async (
};
};
/**
* Synchronously checks whether a strategy needs regeneration.
* Strategies can implement `shouldRegenerate` for custom logic or declare
* `registryDependencies` to opt in to the default reference-equality check.
* The default list (entities, devices, areas, floors) is used when neither is
* provided, preserving the previous behavior for third-party strategies.
*/
export const checkStrategyShouldRegenerate = (
configType: LovelaceStrategyConfigType,
strategyConfig: LovelaceStrategyConfig,
oldHass: HomeAssistant,
newHass: HomeAssistant
): boolean => {
const strategyType = strategyConfig.type;
if (!strategyType) {
return false;
}
const tags = getStrategyTag(configType, strategyType);
const strategy = tags
? ((customElements.get(tags.tag) ??
(tags.type === "custom"
? customElements.get(tags.legacyTag)
: undefined)) as unknown as LovelaceStrategy | undefined)
: undefined;
if (strategy?.shouldRegenerate) {
return strategy.shouldRegenerate(strategyConfig, oldHass, newHass);
}
const dependencies =
strategy?.registryDependencies ?? DEFAULT_REGISTRY_DEPENDENCIES;
return dependencies.some((key) => oldHass[key] !== newHass[key]);
};
/**
* Find all references to strategies and replaces them with the generated output
*/
@@ -17,7 +17,6 @@ import type {
} from "../../cards/types";
import type { ButtonHeadingBadgeConfig } from "../../heading-badges/types";
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
import type { LovelaceStrategyDependency } from "../types";
import {
getSummaryLabel,
HOME_SUMMARIES,
@@ -34,13 +33,6 @@ export interface HomeAreaViewStrategyConfig {
@customElement("home-area-view-strategy")
export class HomeAreaViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [
"entities",
"devices",
"areas",
"panels",
];
static async generate(
config: HomeAreaViewStrategyConfig,
hass: HomeAssistant
@@ -4,10 +4,7 @@ import { customElement } from "lit/decorators";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type {
LovelaceStrategyEditor,
LovelaceStrategyDependency,
} from "../types";
import type { LovelaceStrategyEditor } from "../types";
import {
getSummaryLabel,
HOME_SUMMARIES_ICONS,
@@ -28,10 +25,6 @@ export interface HomeDashboardStrategyConfig {
@customElement("home-dashboard-strategy")
export class HomeDashboardStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [
"areas",
];
static async generate(
config: HomeDashboardStrategyConfig,
hass: HomeAssistant
@@ -10,7 +10,6 @@ import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { MediaControlCardConfig } from "../../cards/types";
import type { LovelaceStrategyDependency } from "../types";
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
@@ -81,13 +80,6 @@ const processUnassignedEntities = (
@customElement("home-media-players-view-strategy")
export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [
"entities",
"devices",
"areas",
"floors",
];
static async generate(
_config: HomeMediaPlayersViewStrategyConfig,
hass: HomeAssistant
@@ -15,7 +15,6 @@ import type {
EntitiesCardConfig,
HeadingCardConfig,
} from "../../cards/types";
import type { LovelaceStrategyDependency } from "../types";
import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
export interface HomeOtherDevicesViewStrategyConfig {
@@ -25,13 +24,6 @@ export interface HomeOtherDevicesViewStrategyConfig {
@customElement("home-other-devices-view-strategy")
export class HomeOtherDevicesViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [
"entities",
"devices",
"areas",
"floors",
];
static async generate(
config: HomeOtherDevicesViewStrategyConfig,
hass: HomeAssistant
@@ -35,7 +35,6 @@ import {
LARGE_SCREEN_CONDITION,
SMALL_SCREEN_CONDITION,
} from "../helpers/view-columns-conditions";
import type { LovelaceStrategyDependency } from "../types";
import type { CommonControlsSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
@@ -80,14 +79,6 @@ const computeAreaCard = (
@customElement("home-overview-view-strategy")
export class HomeOverviewViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [
"entities",
"devices",
"areas",
"floors",
"panels",
];
static async generate(
config: HomeOverviewViewStrategyConfig,
hass: HomeAssistant
@@ -1,18 +1,13 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type {
LovelaceStrategyEditor,
LovelaceStrategyDependency,
} from "../types";
import type { LovelaceStrategyEditor } from "../types";
import type { IframeViewStrategyConfig } from "./iframe-view-strategy";
export type IframeDashboardStrategyConfig = IframeViewStrategyConfig;
@customElement("iframe-dashboard-strategy")
export class IframeDashboardStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
config: IframeDashboardStrategyConfig
): Promise<LovelaceConfig> {
@@ -2,7 +2,6 @@ import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { IframeCardConfig } from "../../cards/types";
import type { LovelaceStrategyDependency } from "../types";
export interface IframeViewStrategyConfig {
type: "iframe";
@@ -12,8 +11,6 @@ export interface IframeViewStrategyConfig {
@customElement("iframe-view-strategy")
export class IframeViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
config: IframeViewStrategyConfig
): Promise<LovelaceViewConfig> {
@@ -3,15 +3,12 @@ import { customElement } from "lit/decorators";
import type { LovelaceDashboardSuggestions } from "../../../../data/lovelace/dashboard";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceStrategyDependency } from "../types";
import type { MapViewStrategyConfig } from "./map-view-strategy";
export type MapDashboardStrategyConfig = MapViewStrategyConfig;
@customElement("map-dashboard-strategy")
export class MapDashboardStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
config: MapDashboardStrategyConfig
): Promise<LovelaceConfig> {
@@ -3,7 +3,6 @@ import { customElement } from "lit/decorators";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { MapCardConfig } from "../../cards/types";
import type { LovelaceStrategyDependency } from "../types";
export interface MapViewStrategyConfig {
type: "map";
@@ -11,8 +10,6 @@ export interface MapViewStrategyConfig {
@customElement("map-view-strategy")
export class MapViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: MapViewStrategyConfig,
hass: HomeAssistant
@@ -1,10 +1,7 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type {
LovelaceStrategyEditor,
LovelaceStrategyDependency,
} from "../types";
import type { LovelaceStrategyEditor } from "../types";
import type { OriginalStatesViewStrategyConfig } from "./original-states-view-strategy";
export type OriginalStatesDashboardStrategyConfig =
@@ -12,8 +9,6 @@ export type OriginalStatesDashboardStrategyConfig =
@customElement("original-states-dashboard-strategy")
export class OriginalStatesDashboardStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
config: OriginalStatesDashboardStrategyConfig
): Promise<LovelaceConfig> {
@@ -9,7 +9,6 @@ import type { HomeAssistant } from "../../../../types";
import type { EmptyStateCardConfig } from "../../cards/types";
import { generateDefaultViewConfig } from "../../common/generate-lovelace-config";
import { computeDomain } from "../../../../common/entity/compute_domain";
import type { LovelaceStrategyDependency } from "../types";
export interface OriginalStatesViewStrategyConfig {
type: "original-states";
@@ -20,13 +19,6 @@ export interface OriginalStatesViewStrategyConfig {
@customElement("original-states-view-strategy")
export class OriginalStatesViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [
"entities",
"devices",
"areas",
"floors",
];
static async generate(
config: OriginalStatesViewStrategyConfig,
hass: HomeAssistant
-14
View File
@@ -6,22 +6,8 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { LovelaceGenericElementEditor } from "../types";
export type LovelaceStrategyDependency =
| "entities"
| "devices"
| "areas"
| "floors"
| "labels"
| "panels";
export interface LovelaceStrategy<T = any> {
generate(config: LovelaceStrategyConfig, hass: HomeAssistant): Promise<T>;
shouldRegenerate?(
config: LovelaceStrategyConfig,
oldHass: HomeAssistant,
newHass: HomeAssistant
): boolean;
registryDependencies?: readonly LovelaceStrategyDependency[];
getConfigElement?: () => LovelaceStrategyEditor;
noEditor?: boolean;
configRequired?: boolean;
@@ -6,7 +6,6 @@ import { getCommonControlsUsagePrediction } from "../../../../data/usage_predict
import type { HomeAssistant } from "../../../../types";
import type { HeadingCardConfig, TileCardConfig } from "../../cards/types";
import type { Condition } from "../../common/validate-condition";
import type { LovelaceStrategyDependency } from "../types";
const DEFAULT_LIMIT = 8;
@@ -34,8 +33,6 @@ const toTileCard = (entity: string): TileCardConfig => ({
@customElement("common-controls-section-strategy")
export class CommonControlsSectionStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
config: CommonControlsSectionStrategyConfig,
hass: HomeAssistant
+5 -11
View File
@@ -42,10 +42,7 @@ import { parseLovelaceCardPath } from "../editor/lovelace-path";
import { createErrorSectionConfig } from "../sections/hui-error-section";
import "../sections/hui-section";
import type { HuiSection } from "../sections/hui-section";
import {
checkStrategyShouldRegenerate,
generateLovelaceViewStrategy,
} from "../strategies/get-strategy";
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
import type { Lovelace } from "../types";
import { getViewType } from "./get-view-type";
@@ -188,13 +185,10 @@ export class HUIView extends ReactiveElement {
if (oldHass && this.hass && this.lovelace && isStrategyView(viewConfig)) {
if (
this.hass.config.state === "RUNNING" &&
(oldHass.config.state !== "RUNNING" ||
checkStrategyShouldRegenerate(
"view",
viewConfig.strategy,
oldHass,
this.hass
))
(oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors)
) {
this._debounceRefreshConfig();
}
+1 -1
View File
@@ -11213,7 +11213,7 @@
},
"landing-page": {
"header": "Preparing Home Assistant",
"subheader": "The latest version of Home Assistant is being downloaded. This may take 20 minutes or more.",
"subheader": "This may take 20 minutes or more",
"show_details": "Show details",
"hide_details": "Hide details",
"network_issue": {
+113 -113
View File
@@ -4041,161 +4041,161 @@ __metadata:
languageName: node
linkType: hard
"@tsparticles/basic@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/basic@npm:4.1.1"
"@tsparticles/basic@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/basic@npm:4.1.0"
dependencies:
"@tsparticles/engine": "npm:4.1.1"
"@tsparticles/plugin-blend": "npm:4.1.1"
"@tsparticles/plugin-hex-color": "npm:4.1.1"
"@tsparticles/plugin-hsl-color": "npm:4.1.1"
"@tsparticles/plugin-move": "npm:4.1.1"
"@tsparticles/plugin-rgb-color": "npm:4.1.1"
"@tsparticles/shape-circle": "npm:4.1.1"
"@tsparticles/updater-opacity": "npm:4.1.1"
"@tsparticles/updater-out-modes": "npm:4.1.1"
"@tsparticles/updater-paint": "npm:4.1.1"
"@tsparticles/updater-size": "npm:4.1.1"
checksum: 10/99191aeee4b9a3856aa82a2cea21e54d2099b6d58b4af3bacf4bb133277bd71de16ac07fe632ebabe08cc2a06be1aa6c00d82d593027276bf588870afc9182f5
"@tsparticles/engine": "npm:4.1.0"
"@tsparticles/plugin-blend": "npm:4.1.0"
"@tsparticles/plugin-hex-color": "npm:4.1.0"
"@tsparticles/plugin-hsl-color": "npm:4.1.0"
"@tsparticles/plugin-move": "npm:4.1.0"
"@tsparticles/plugin-rgb-color": "npm:4.1.0"
"@tsparticles/shape-circle": "npm:4.1.0"
"@tsparticles/updater-opacity": "npm:4.1.0"
"@tsparticles/updater-out-modes": "npm:4.1.0"
"@tsparticles/updater-paint": "npm:4.1.0"
"@tsparticles/updater-size": "npm:4.1.0"
checksum: 10/aae6ebde0377e9f5b2a150cc94c7e140012d61d33ce0b2ab135e2513a106dad923f4704db67e29f912374600dc474c206697a06d846173141e4830f89cff2661
languageName: node
linkType: hard
"@tsparticles/canvas-utils@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/canvas-utils@npm:4.1.1"
"@tsparticles/canvas-utils@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/canvas-utils@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/2b5d7e9a55aa8086007f2dff940800c650233751ebb708fdbf9f91be4b0cd9d975400da42cdf531bb74860974dff5ea3c76199520ccd3f089904fba0d1bc0722
"@tsparticles/engine": 4.1.0
checksum: 10/bdde613b64665756080e9937bd4013a4e026e23baea1e536a9ae6785ff1b14bef5bbebca4eed6a10404cffcc375737fc101cb27f068146b983a34b7967a1f729
languageName: node
linkType: hard
"@tsparticles/engine@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/engine@npm:4.1.1"
checksum: 10/74886de63046f8752515f097176cae2fa8d506fd9d2d6a84106d43a89ff688e047d99a5018e56e3fffc17d5d13a9061f63fafcb2b4ebe773f78379621d0d9855
"@tsparticles/engine@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/engine@npm:4.1.0"
checksum: 10/9b8fc1e8f6ae67541d8c230996af9c27e1da04ba2d8d6f9daf5de4f321a523dfc968a0fab033708c471a9cde6ea0f1c5634cbb5ef3a3d27ef4b182dc193212f1
languageName: node
linkType: hard
"@tsparticles/interaction-particles-links@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/interaction-particles-links@npm:4.1.1"
"@tsparticles/interaction-particles-links@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/interaction-particles-links@npm:4.1.0"
dependencies:
"@tsparticles/canvas-utils": "npm:4.1.1"
"@tsparticles/canvas-utils": "npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
"@tsparticles/plugin-interactivity": 4.1.1
checksum: 10/c0c7ad3740f2168b75ca7ae09341fefcc1d8c8d117fbee7a6519a622843dfe5d57bb65113a4eba40f26906904f069cbac196840f45fc7be12864fddfa8106184
"@tsparticles/engine": 4.1.0
"@tsparticles/plugin-interactivity": 4.1.0
checksum: 10/2f74b25a1e585e6427034e38daba851880e2ae342da4558e3a9910bd37e0fb812438e19252ce11807dd0ed8bd53c3a5e48329b04450eac672310d1e058f720db
languageName: node
linkType: hard
"@tsparticles/plugin-blend@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-blend@npm:4.1.1"
"@tsparticles/plugin-blend@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/plugin-blend@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/9a666dc1fe0ed9ff4743fc23ca3bf27bd5d66073cc0127203b566077d431321b61af440875e96c87950e18011b1e6b9b43d328e4200923dd4be9fd934c07a5cd
"@tsparticles/engine": 4.1.0
checksum: 10/e1913556ada7d70bbcd97b9591315fb15dc29edad3e31f50ec26639d225a15b5c237108a06173796f8a8bbb87e5f3f37ed40f3dece77f86bee4bd274600f813e
languageName: node
linkType: hard
"@tsparticles/plugin-hex-color@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-hex-color@npm:4.1.1"
"@tsparticles/plugin-hex-color@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/plugin-hex-color@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/33fde7c2763affb1dd7fa033431a2553646bde79c9ab52106be6226a9716ae973c90c8b4bf381d96f661ab75467934019cd456fe27ccac59dca11b5c989c3a75
"@tsparticles/engine": 4.1.0
checksum: 10/611992f350170e97daff323cecfa5f56625fbce0033d94dce5e579a5433ce9c46b1ceeb6381a714b58296fbd2802be806b8505d950af881f02e02ef8aac32f30
languageName: node
linkType: hard
"@tsparticles/plugin-hsl-color@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-hsl-color@npm:4.1.1"
"@tsparticles/plugin-hsl-color@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/plugin-hsl-color@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/594f4a840f6f6f134a11d76c1a7fa80ed60a5d945534aad53e8ab5718341187f0fac73eb45d1c39246b98e982f1d60f9dbb95a083edcf8407e015048056b84e7
"@tsparticles/engine": 4.1.0
checksum: 10/6e26d48733df47f5a91315b0144485440530fb5a6250b02254481157f05355ec59ded1fbfff6463d4dac40cb594a5ded660606a07b9376ce5f083eb92ff9aeac
languageName: node
linkType: hard
"@tsparticles/plugin-interactivity@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-interactivity@npm:4.1.1"
"@tsparticles/plugin-interactivity@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/plugin-interactivity@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/448bd8f7c741ed0359ace49a6c58d58947d95660158a131c86c248427b9caa41645c6e8c9cea6a8f0f6a70cd25c0dd64017edba79c8f225b6db07f39b660eb4d
"@tsparticles/engine": 4.1.0
checksum: 10/7fe688b0531395e4f2e4f103bdd490471fa3e65c12ea98ac420f5380d5b00d460b99407ada19f3c2f1dbcedaffe061c399e43446cbd684e9e4df5c112e08baee
languageName: node
linkType: hard
"@tsparticles/plugin-move@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-move@npm:4.1.1"
"@tsparticles/plugin-move@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/plugin-move@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/b854cb6e2dcea2971f1abaca75c6e8cde40611178f13513559f60cc45da568e8b61c2f027619041d94e11c60af08a76eb4957d344a910b7a6255265937199094
"@tsparticles/engine": 4.1.0
checksum: 10/97dc5d4ecde770c1d7fc7b3bb2dc5075eb44405e2c541ab6a64e07c14a6aebf038ad4bf0dd7583606c63908a9e58b92a140acc9e437862376975d8c5b52dfc94
languageName: node
linkType: hard
"@tsparticles/plugin-rgb-color@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-rgb-color@npm:4.1.1"
"@tsparticles/plugin-rgb-color@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/plugin-rgb-color@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/2372dfe5ceec163b49c02b3b0169e6032fae34e4ade3c29d42bd26af23fac76b3416d70eed9cac9a0f2b470c763ee903d12d0e9156b7693b3c8908e25714cf76
"@tsparticles/engine": 4.1.0
checksum: 10/1635482f8aa8664779fd30eaaab3cfd6d7ac53b2f5dafbbefd26a9151610eb40326c5240ed5439cb3f5e426a7111e80441554fd8a9848b8d6fe5176cb243ed0e
languageName: node
linkType: hard
"@tsparticles/preset-links@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/preset-links@npm:4.1.1"
"@tsparticles/preset-links@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/preset-links@npm:4.1.0"
dependencies:
"@tsparticles/basic": "npm:4.1.1"
"@tsparticles/engine": "npm:4.1.1"
"@tsparticles/interaction-particles-links": "npm:4.1.1"
"@tsparticles/plugin-interactivity": "npm:4.1.1"
checksum: 10/26dd1dcd4ede3bae32cae3585a7f929e0089a75e389f83cb5b00213e977b647c3e7e743b5e83d3a5400beffcff9343bde7538412579708bad761d2e933708638
"@tsparticles/basic": "npm:4.1.0"
"@tsparticles/engine": "npm:4.1.0"
"@tsparticles/interaction-particles-links": "npm:4.1.0"
"@tsparticles/plugin-interactivity": "npm:4.1.0"
checksum: 10/12e7da30de505a0f10f8caeafebdd6393e998cd3159d1b478cf28720a14f4eadd28ee82630eb08e10962987e8da3114f638dc500994fb45cff8d3c64e75433fb
languageName: node
linkType: hard
"@tsparticles/shape-circle@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/shape-circle@npm:4.1.1"
"@tsparticles/shape-circle@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/shape-circle@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/401e8a267cf9301dae8386e9bb1c5ff3a02dfb5b5f136187c73ed6b89e33f176f929fdc53acaa60b13f3ee1ed7f8c6f35f3f0e26ff6930a33d1949a0f23e4c1f
"@tsparticles/engine": 4.1.0
checksum: 10/eff8a96c1816e183079b93b5734f7055cf85c4cbc42cc1cdd759ec114303d7c532299827bee6754644387b4894a78b9fb593551a3ef86e39f49eaef943ce856b
languageName: node
linkType: hard
"@tsparticles/updater-opacity@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/updater-opacity@npm:4.1.1"
"@tsparticles/updater-opacity@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/updater-opacity@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/3fe157203e02dfec1ef9ac693d517cd9abcad8c28acaac8d4ca923b8301a99ae73129ed4559ea507c0d7d6ad626bdbbdb03f25868d7d28f852e2d8ffc7d13790
"@tsparticles/engine": 4.1.0
checksum: 10/67394b4e63017db0ca758025ec2283c46ad09689310b6872d486395db77f8432de34a1a145547b8248a279d4becbda8d586684d44ddce8917ef3ac8b4f15a383
languageName: node
linkType: hard
"@tsparticles/updater-out-modes@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/updater-out-modes@npm:4.1.1"
"@tsparticles/updater-out-modes@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/updater-out-modes@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/bc5f074661c42acc20a87ee3ff093fe63d2fd87a7f7d3156c2fb23dc1d24881ccc9e9cb82313ff7265317fcdc447a5b17b2204930faf8ba627278e2dc0fab2c3
"@tsparticles/engine": 4.1.0
checksum: 10/e308dbf854d65eb87fadd43c80e2c2d578399ea2e446327f2b5956a57d5c135e258251a4bdeaceace44d169abf326eabd8f7c4bcfcb027dcde2942b2a28c9ea4
languageName: node
linkType: hard
"@tsparticles/updater-paint@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/updater-paint@npm:4.1.1"
"@tsparticles/updater-paint@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/updater-paint@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/77459f6337b869d654573696dbe2bc1d238ddf3410857d8b91df606f7402301acae34f4a334727faa552f9816f518353b7a98e0bda4854ea5419cb34e8d447d2
"@tsparticles/engine": 4.1.0
checksum: 10/212e389e9e728a2d69a706bc606d6f6e6912ebd562fe4428236e1b72af3b49cf19043a4b72a26091230ab3ec128ed99cd05a8e6b70ad8cff02413d3dcdaabb6d
languageName: node
linkType: hard
"@tsparticles/updater-size@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/updater-size@npm:4.1.1"
"@tsparticles/updater-size@npm:4.1.0":
version: 4.1.0
resolution: "@tsparticles/updater-size@npm:4.1.0"
peerDependencies:
"@tsparticles/engine": 4.1.1
checksum: 10/33ae9d51394e299459478a9dedd369de1bd266ad9c2e2236ec2c20bb455aad9118efa13afc7d359b6d7d010a34bba5b60cbf2e3d8b853e124c0b53c2aae8db18
"@tsparticles/engine": 4.1.0
checksum: 10/8e513044cbc4fa84e00008a77d0069cdebcf5efed0414aad5ad2a09497226182fac9d85037594853468d0ef2ae210616852fecf35abef44d89edf24fedf87c51
languageName: node
linkType: hard
@@ -7957,10 +7957,10 @@ __metadata:
languageName: node
linkType: hard
"fuse.js@npm:7.4.0":
version: 7.4.0
resolution: "fuse.js@npm:7.4.0"
checksum: 10/dba0ef239be1f28ba5daefb3a17371c73291f4d0db3d1733b625848a7311e05aa58a795cd5b2fd9626c09857608a74e8c9620f5d1ce2d3d0b2d40155ae15e21e
"fuse.js@npm:7.3.0":
version: 7.3.0
resolution: "fuse.js@npm:7.3.0"
checksum: 10/b2cdc39e46acb9524fe900356af74c987ecb1dbc67412df651a5291fa072d212e6d74457f0b5d1c39baf79539481f62b613ac792afd8995dd1fc3d20b5354914
languageName: node
linkType: hard
@@ -8487,8 +8487,8 @@ __metadata:
"@rspack/dev-server": "npm:2.0.3"
"@swc/helpers": "npm:0.5.23"
"@thomasloven/round-slider": "npm:0.6.0"
"@tsparticles/engine": "npm:4.1.1"
"@tsparticles/preset-links": "npm:4.1.1"
"@tsparticles/engine": "npm:4.1.0"
"@tsparticles/preset-links": "npm:4.1.0"
"@types/chromecast-caf-receiver": "npm:6.0.26"
"@types/chromecast-caf-sender": "npm:1.0.11"
"@types/color-name": "npm:2.0.0"
@@ -8534,7 +8534,7 @@ __metadata:
eslint-plugin-wc: "npm:3.1.0"
fancy-log: "npm:2.0.0"
fs-extra: "npm:11.3.5"
fuse.js: "npm:7.4.0"
fuse.js: "npm:7.3.0"
generate-license-file: "npm:4.2.1"
glob: "npm:13.0.6"
globals: "npm:17.6.0"
@@ -8557,7 +8557,7 @@ __metadata:
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
leaflet.markercluster: "npm:1.5.3"
license-checker-rseidelsohn: "npm:5.0.1"
lint-staged: "npm:17.0.7"
lint-staged: "npm:17.0.5"
lit: "npm:3.3.3"
lit-analyzer: "npm:2.0.3"
lit-html: "npm:3.3.3"
@@ -9936,21 +9936,21 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:17.0.7":
version: 17.0.7
resolution: "lint-staged@npm:17.0.7"
"lint-staged@npm:17.0.5":
version: 17.0.5
resolution: "lint-staged@npm:17.0.5"
dependencies:
listr2: "npm:^10.2.1"
picomatch: "npm:^4.0.4"
string-argv: "npm:^0.3.2"
tinyexec: "npm:^1.2.4"
yaml: "npm:^2.9.0"
tinyexec: "npm:^1.1.2"
yaml: "npm:^2.8.4"
dependenciesMeta:
yaml:
optional: true
bin:
lint-staged: bin/lint-staged.js
checksum: 10/4ed3cd01caa78ff5cc5da7ec69f77f091c43a0d5cbb1e084f7ffd3872a9e599675fb8b5f11fd5911faee0d330952889dd0e14378a26620d8f529eae401ce49b4
checksum: 10/a0bea43689d68ec0bf6a56943884dbdb96b6b49e2677bf80654d802678b2edf9fc65338ca8ef3fc310f245933ea2a809db1ac94431dc445c57a4d49620d9d4da
languageName: node
linkType: hard
@@ -13172,10 +13172,10 @@ __metadata:
languageName: node
linkType: hard
"tinyexec@npm:^1.0.2, tinyexec@npm:^1.2.4":
version: 1.2.4
resolution: "tinyexec@npm:1.2.4"
checksum: 10/f20b3e6f56f24c3ebe0129d0b6e657e561d225df2cf93c1a10362996232dd6ad4b8af8c9e81d258a64d09020e723772baf6fe0be26512dba7c61bb366d67b1f9
"tinyexec@npm:^1.0.2, tinyexec@npm:^1.1.2":
version: 1.1.2
resolution: "tinyexec@npm:1.1.2"
checksum: 10/2bbe37f9001c6f5723ab39eb8dc1e88f77e830d7cf2e8f34bb75019eb505fcfe3b061b4799c502ff31fa63aa1a9adc649add5ff1e17b7fbd8c16e1afb75d0b9e
languageName: node
linkType: hard
@@ -14755,7 +14755,7 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.9.0":
"yaml@npm:^2.8.4":
version: 2.9.0
resolution: "yaml@npm:2.9.0"
bin: