mirror of
https://github.com/home-assistant/frontend.git
synced 2026-11-06 04:54:12 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f075d544b8 |
No files matched your search
@@ -60,6 +60,9 @@ export interface ClimateFrontendSystemData {
|
||||
hidden_entities?: string[];
|
||||
}
|
||||
|
||||
export type HomeSummaryPanelSystemDataKey =
|
||||
"security" | "maintenance" | "climate";
|
||||
|
||||
export interface EnergyFrontendSystemData {
|
||||
// Stable "<view>.<card-type>" keys of energy dashboard cards the user has
|
||||
// hidden. An absent key or array means nothing is hidden (all cards visible),
|
||||
|
||||
@@ -1,313 +1,9 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import {
|
||||
fetchFrontendSystemDataAfterSaves,
|
||||
queueFrontendSystemDataSave,
|
||||
type ClimateFrontendSystemData,
|
||||
} from "../../data/frontend";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { ChildPanelReady } from "../../layouts/panel-ready";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-background";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
|
||||
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
type: "climate",
|
||||
},
|
||||
};
|
||||
import { customElement } from "lit/decorators";
|
||||
import { HaHomeSummaryPanelBase } from "../home/ha-home-summary-panel-base";
|
||||
|
||||
@customElement("ha-panel-climate")
|
||||
class PanelClimate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state() private _viewIndex = 0;
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _config?: ClimateFrontendSystemData;
|
||||
|
||||
@state() private _editVisibility = false;
|
||||
|
||||
@state() private _searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
private _childPanelReady?: ChildPanelReady;
|
||||
|
||||
private _loadConfigPromise?: Promise<void>;
|
||||
|
||||
private _loadConfigRevision = 0;
|
||||
|
||||
private _setLovelaceRevision = 0;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (
|
||||
oldHass &&
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && this.hass) {
|
||||
// Refresh the generated view when registries or panels change.
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
await Promise.all([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
this._loadConfigPromise,
|
||||
]);
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
const revision = ++this._loadConfigRevision;
|
||||
this._config = undefined;
|
||||
try {
|
||||
const data = await fetchFrontendSystemDataAfterSaves(
|
||||
this.hass.connection,
|
||||
"climate"
|
||||
);
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
this._config = data || {};
|
||||
} catch (err) {
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load climate configuration:", err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.climate.editor.load_failed"),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
);
|
||||
|
||||
private _registriesChanged = async () => {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${this._searchParams.has("historyBack")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.climate")}</div>
|
||||
${
|
||||
!this.hass.user?.is_admin
|
||||
? nothing
|
||||
: this._editVisibility
|
||||
? html`<ha-button
|
||||
slot="actionItems"
|
||||
appearance="filled"
|
||||
size="s"
|
||||
@click=${this._stopEditVisibility}
|
||||
>
|
||||
${this.hass.localize("ui.panel.lovelace.menu.exit_edit_mode")}
|
||||
</ha-button>`
|
||||
: this._config
|
||||
? html`<ha-icon-button
|
||||
slot="actionItems"
|
||||
.path=${mdiPencil}
|
||||
.label=${this.hass.localize("ui.panel.climate.editor.title")}
|
||||
@click=${this._startEditVisibility}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}>
|
||||
</hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
@entity-visibility-changed=${this._visibilityChanged}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLovelace(force = false) {
|
||||
const revision = ++this._setLovelaceRevision;
|
||||
if (this._loadConfigPromise) {
|
||||
await this._loadConfigPromise;
|
||||
}
|
||||
|
||||
const viewConfig = await generateLovelaceViewStrategy(
|
||||
{
|
||||
strategy: {
|
||||
...CLIMATE_LOVELACE_VIEW_CONFIG.strategy,
|
||||
hidden_entities: this._config?.hidden_entities,
|
||||
edit_visibility: this._editVisibility || undefined,
|
||||
},
|
||||
},
|
||||
this.hass
|
||||
);
|
||||
|
||||
if (revision !== this._setLovelaceRevision) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { views: [viewConfig] };
|
||||
const rawConfig = { views: [CLIMATE_LOVELACE_VIEW_CONFIG] };
|
||||
|
||||
if (!force && deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._childPanelReady ??= new ChildPanelReady(this);
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
editMode: false,
|
||||
urlPath: "climate",
|
||||
mode: "generated",
|
||||
locale: this.hass.locale,
|
||||
enableFullEditMode: () => undefined,
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
showToast: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private _startEditVisibility = () => {
|
||||
this._editVisibility = true;
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _stopEditVisibility = () => {
|
||||
this._editVisibility = false;
|
||||
if (!this._config) {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
}
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _visibilityChanged = (
|
||||
ev: HASSDomEvent<HASSDomEvents["entity-visibility-changed"]>
|
||||
) => {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const { entity, hidden } = ev.detail;
|
||||
const hiddenEntities = (this._config.hidden_entities ?? []).filter(
|
||||
(entityId) => entityId !== entity
|
||||
);
|
||||
if (hidden) {
|
||||
hiddenEntities.push(entity);
|
||||
}
|
||||
this._saveConfig({
|
||||
...this._config,
|
||||
hidden_entities: hiddenEntities.length ? hiddenEntities : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
private _saveConfig(config: ClimateFrontendSystemData): Promise<void> {
|
||||
this._config = config;
|
||||
return queueFrontendSystemDataSave(
|
||||
this.hass.connection,
|
||||
"climate",
|
||||
config
|
||||
).catch(async (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to save climate configuration:", err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.climate.editor.save_failed"),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
if (config !== this._config) {
|
||||
return;
|
||||
}
|
||||
await this._loadConfig();
|
||||
await this._setLovelace(true);
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
user-select: none;
|
||||
}
|
||||
hui-view-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
hui-view {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
class PanelClimate extends HaHomeSummaryPanelBase<"climate"> {
|
||||
protected readonly _systemDataKey = "climate";
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import type { HaButton } from "../../components/ha-button";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import {
|
||||
fetchFrontendSystemDataAfterSaves,
|
||||
queueFrontendSystemDataSave,
|
||||
type HomeSummaryPanelSystemDataKey,
|
||||
} from "../../data/frontend";
|
||||
import { ChildPanelReady } from "../../layouts/panel-ready";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { clearEntityVisibilityFocus } from "../lovelace/cards/hui-entity-visibility-card";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-background";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
|
||||
export abstract class HaHomeSummaryPanelBase<
|
||||
K extends HomeSummaryPanelSystemDataKey,
|
||||
> extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state() protected _config?: FrontendSystemData[K];
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _editVisibility = false;
|
||||
|
||||
@state() private _searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@query("ha-button") private _exitEditButton?: HaButton;
|
||||
|
||||
protected abstract readonly _systemDataKey: K;
|
||||
|
||||
private _childPanelReady?: ChildPanelReady;
|
||||
|
||||
private _loadConfigPromise?: Promise<void>;
|
||||
|
||||
private _loadConfigRevision = 0;
|
||||
|
||||
private _setLovelaceRevision = 0;
|
||||
|
||||
protected _strategyOptions(): Record<string, unknown> {
|
||||
return {};
|
||||
}
|
||||
|
||||
protected _favoriteChanged?(
|
||||
ev: HASSDomEvent<HASSDomEvents["entity-favorite-changed"]>
|
||||
): void;
|
||||
|
||||
protected _alertChanged?(
|
||||
ev: HASSDomEvent<HASSDomEvents["entity-alert-changed"]>
|
||||
): void;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (
|
||||
oldHass &&
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
oldHass &&
|
||||
(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) &&
|
||||
this.hass.config.state === "RUNNING"
|
||||
) {
|
||||
this._debounceRegistriesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
await Promise.all([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
this._loadConfigPromise,
|
||||
]);
|
||||
await this._setLovelace();
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
const revision = ++this._loadConfigRevision;
|
||||
this._config = undefined;
|
||||
try {
|
||||
const data = await fetchFrontendSystemDataAfterSaves(
|
||||
this.hass.connection,
|
||||
this._systemDataKey
|
||||
);
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
this._config = data || ({} as FrontendSystemData[K]);
|
||||
} catch (err) {
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
this._editVisibility = false;
|
||||
const key: HomeSummaryPanelSystemDataKey = this._systemDataKey;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to load ${key} configuration:`, err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize(`ui.panel.${key}.editor.load_failed`),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(() => this._setLovelace(), 200);
|
||||
|
||||
protected render() {
|
||||
const key: HomeSummaryPanelSystemDataKey = this._systemDataKey;
|
||||
return html`
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${this._searchParams.has("historyBack")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize(`panel.${key}`)}</div>
|
||||
${
|
||||
!this.hass.user?.is_admin
|
||||
? nothing
|
||||
: this._editVisibility
|
||||
? html`<ha-button
|
||||
slot="actionItems"
|
||||
appearance="filled"
|
||||
size="s"
|
||||
@click=${this._stopEditVisibility}
|
||||
>
|
||||
${this.hass.localize("ui.panel.lovelace.menu.exit_edit_mode")}
|
||||
</ha-button>`
|
||||
: this._config
|
||||
? html`<ha-icon-button
|
||||
slot="actionItems"
|
||||
.path=${mdiPencil}
|
||||
.label=${this.hass.localize(`ui.panel.${key}.editor.title`)}
|
||||
@click=${this._startEditVisibility}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}>
|
||||
</hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${0}
|
||||
@entity-visibility-changed=${this._visibilityChanged}
|
||||
@entity-favorite-changed=${this._favoriteChanged}
|
||||
@entity-alert-changed=${this._alertChanged}
|
||||
@entity-visibility-focus-lost=${this._focusLost}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
protected async _setLovelace(force = false) {
|
||||
const revision = ++this._setLovelaceRevision;
|
||||
if (this._loadConfigPromise) {
|
||||
await this._loadConfigPromise;
|
||||
}
|
||||
|
||||
const strategy = { type: this._systemDataKey };
|
||||
const viewConfig = await generateLovelaceViewStrategy(
|
||||
{
|
||||
strategy: {
|
||||
...strategy,
|
||||
...this._strategyOptions(),
|
||||
hidden_entities: this._config?.hidden_entities,
|
||||
edit_visibility: this._editVisibility || undefined,
|
||||
},
|
||||
},
|
||||
this.hass
|
||||
);
|
||||
|
||||
if (revision !== this._setLovelaceRevision) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { views: [viewConfig] };
|
||||
const rawConfig = { views: [{ strategy }] };
|
||||
|
||||
if (!force && deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._childPanelReady ??= new ChildPanelReady(this);
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
editMode: false,
|
||||
urlPath: this._systemDataKey,
|
||||
mode: "generated",
|
||||
locale: this.hass.locale,
|
||||
enableFullEditMode: () => undefined,
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
showToast: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private _startEditVisibility = () => {
|
||||
clearEntityVisibilityFocus();
|
||||
this._editVisibility = true;
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _stopEditVisibility = () => {
|
||||
this._editVisibility = false;
|
||||
if (!this._config) {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
}
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _focusLost = () => {
|
||||
this._exitEditButton?.focus();
|
||||
};
|
||||
|
||||
private _visibilityChanged = (
|
||||
ev: HASSDomEvent<HASSDomEvents["entity-visibility-changed"]>
|
||||
) => {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const { entity, hidden } = ev.detail;
|
||||
const hiddenEntities = (this._config.hidden_entities ?? []).filter(
|
||||
(entityId) => entityId !== entity
|
||||
);
|
||||
if (hidden) {
|
||||
hiddenEntities.push(entity);
|
||||
}
|
||||
this._saveConfig({
|
||||
...this._config,
|
||||
hidden_entities: hiddenEntities.length ? hiddenEntities : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
protected _saveConfig(config: FrontendSystemData[K]): Promise<void> {
|
||||
this._config = config;
|
||||
const key: HomeSummaryPanelSystemDataKey = this._systemDataKey;
|
||||
return queueFrontendSystemDataSave(
|
||||
this.hass.connection,
|
||||
this._systemDataKey,
|
||||
config
|
||||
).catch(async (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to save ${key} configuration:`, err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize(`ui.panel.${key}.editor.save_failed`),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
if (config !== this._config) {
|
||||
return;
|
||||
}
|
||||
await this._loadConfig();
|
||||
await this._setLovelace(true);
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
user-select: none;
|
||||
}
|
||||
hui-view-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
hui-view {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,8 @@ import {
|
||||
fetchFrontendSystemData,
|
||||
saveFrontendSystemData,
|
||||
subscribeFrontendSystemData,
|
||||
type ClimateFrontendSystemData,
|
||||
type HomeFrontendSystemData,
|
||||
type MaintenanceFrontendSystemData,
|
||||
type SecurityFrontendSystemData,
|
||||
type HomeSummaryPanelSystemDataKey,
|
||||
} from "../../data/frontend";
|
||||
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
@@ -40,6 +38,16 @@ import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
|
||||
import { showNewOverviewDialog } from "./dialogs/show-dialog-new-overview";
|
||||
import { hasLegacyOverviewPanel } from "../../data/panel";
|
||||
|
||||
const SUMMARY_PANEL_KEYS: HomeSummaryPanelSystemDataKey[] = [
|
||||
"security",
|
||||
"maintenance",
|
||||
"climate",
|
||||
];
|
||||
|
||||
type SummaryPanelConfigs = {
|
||||
[K in HomeSummaryPanelSystemDataKey]: FrontendSystemData[K];
|
||||
};
|
||||
|
||||
@customElement("ha-panel-home")
|
||||
class PanelHome extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -54,11 +62,11 @@ class PanelHome extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _config?: HomeFrontendSystemData;
|
||||
|
||||
@state() private _securityConfig: SecurityFrontendSystemData = {};
|
||||
|
||||
@state() private _maintenanceConfig: MaintenanceFrontendSystemData = {};
|
||||
|
||||
@state() private _climateConfig: ClimateFrontendSystemData = {};
|
||||
@state() private _summaryConfigs: SummaryPanelConfigs = {
|
||||
security: {},
|
||||
maintenance: {},
|
||||
climate: {},
|
||||
};
|
||||
|
||||
@state() private _extraActionItems?: ExtraActionItem[];
|
||||
|
||||
@@ -66,60 +74,29 @@ class PanelHome extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _loadConfigPromise?: Promise<void>;
|
||||
|
||||
private _securityConfigRevision = 0;
|
||||
|
||||
private _maintenanceConfigRevision = 0;
|
||||
|
||||
private _climateConfigRevision = 0;
|
||||
private _summaryConfigRevisions: Record<
|
||||
HomeSummaryPanelSystemDataKey,
|
||||
number
|
||||
> = {
|
||||
security: 0,
|
||||
maintenance: 0,
|
||||
climate: 0,
|
||||
};
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeFrontendSystemData(
|
||||
this.hass.connection,
|
||||
"security",
|
||||
({ value }) => {
|
||||
this._securityConfigRevision++;
|
||||
const config = value || {};
|
||||
if (deepEqual(config, this._securityConfig)) {
|
||||
return;
|
||||
}
|
||||
this._securityConfig = config;
|
||||
if (this.hasUpdated) {
|
||||
this._setLovelace();
|
||||
}
|
||||
return SUMMARY_PANEL_KEYS.map((key) =>
|
||||
subscribeFrontendSystemData(this.hass.connection, key, ({ value }) => {
|
||||
this._summaryConfigRevisions[key]++;
|
||||
const config = value || {};
|
||||
if (deepEqual(config, this._summaryConfigs[key])) {
|
||||
return;
|
||||
}
|
||||
),
|
||||
subscribeFrontendSystemData(
|
||||
this.hass.connection,
|
||||
"maintenance",
|
||||
({ value }) => {
|
||||
this._maintenanceConfigRevision++;
|
||||
const config = value || {};
|
||||
if (deepEqual(config, this._maintenanceConfig)) {
|
||||
return;
|
||||
}
|
||||
this._maintenanceConfig = config;
|
||||
if (this.hasUpdated) {
|
||||
this._setLovelace();
|
||||
}
|
||||
this._summaryConfigs = { ...this._summaryConfigs, [key]: config };
|
||||
if (this.hasUpdated) {
|
||||
this._setLovelace();
|
||||
}
|
||||
),
|
||||
subscribeFrontendSystemData(
|
||||
this.hass.connection,
|
||||
"climate",
|
||||
({ value }) => {
|
||||
this._climateConfigRevision++;
|
||||
const config = value || {};
|
||||
if (deepEqual(config, this._climateConfig)) {
|
||||
return;
|
||||
}
|
||||
this._climateConfig = config;
|
||||
if (this.hasUpdated) {
|
||||
this._setLovelace();
|
||||
}
|
||||
}
|
||||
),
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private get _showBanner(): boolean {
|
||||
@@ -218,22 +195,15 @@ class PanelHome extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
const securityConfigRevision = this._securityConfigRevision;
|
||||
const maintenanceConfigRevision = this._maintenanceConfigRevision;
|
||||
const climateConfigRevision = this._climateConfigRevision;
|
||||
const [
|
||||
translationsResult,
|
||||
homeResult,
|
||||
securityResult,
|
||||
maintenanceResult,
|
||||
climateResult,
|
||||
] = await Promise.allSettled([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
fetchFrontendSystemData(this.hass.connection, "home"),
|
||||
fetchFrontendSystemData(this.hass.connection, "security"),
|
||||
fetchFrontendSystemData(this.hass.connection, "maintenance"),
|
||||
fetchFrontendSystemData(this.hass.connection, "climate"),
|
||||
]);
|
||||
const summaryRevisions = { ...this._summaryConfigRevisions };
|
||||
const [translationsResult, homeResult, ...summaryResults] =
|
||||
await Promise.allSettled([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
fetchFrontendSystemData(this.hass.connection, "home"),
|
||||
...SUMMARY_PANEL_KEYS.map((key) =>
|
||||
fetchFrontendSystemData(this.hass.connection, key)
|
||||
),
|
||||
]);
|
||||
|
||||
if (translationsResult.status === "rejected") {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -251,44 +221,20 @@ class PanelHome extends SubscribeMixin(LitElement) {
|
||||
this._config = homeResult.value || {};
|
||||
}
|
||||
|
||||
if (securityResult.status === "rejected") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Failed to load security configuration:",
|
||||
securityResult.reason
|
||||
);
|
||||
if (securityConfigRevision === this._securityConfigRevision) {
|
||||
this._securityConfig = {};
|
||||
const summaryConfigs = { ...this._summaryConfigs };
|
||||
SUMMARY_PANEL_KEYS.forEach((key, index) => {
|
||||
if (summaryRevisions[key] !== this._summaryConfigRevisions[key]) {
|
||||
return;
|
||||
}
|
||||
} else if (securityConfigRevision === this._securityConfigRevision) {
|
||||
this._securityConfig = securityResult.value || {};
|
||||
}
|
||||
|
||||
if (maintenanceResult.status === "rejected") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Failed to load maintenance configuration:",
|
||||
maintenanceResult.reason
|
||||
);
|
||||
if (maintenanceConfigRevision === this._maintenanceConfigRevision) {
|
||||
this._maintenanceConfig = {};
|
||||
const result = summaryResults[index];
|
||||
if (result.status === "rejected") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to load ${key} configuration:`, result.reason);
|
||||
}
|
||||
} else if (maintenanceConfigRevision === this._maintenanceConfigRevision) {
|
||||
this._maintenanceConfig = maintenanceResult.value || {};
|
||||
}
|
||||
|
||||
if (climateResult.status === "rejected") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Failed to load climate configuration:",
|
||||
climateResult.reason
|
||||
);
|
||||
if (climateConfigRevision === this._climateConfigRevision) {
|
||||
this._climateConfig = {};
|
||||
}
|
||||
} else if (climateConfigRevision === this._climateConfigRevision) {
|
||||
this._climateConfig = climateResult.value || {};
|
||||
}
|
||||
summaryConfigs[key] =
|
||||
result.status === "fulfilled" && result.value ? result.value : {};
|
||||
});
|
||||
this._summaryConfigs = summaryConfigs;
|
||||
}
|
||||
|
||||
private _debounceRegenerateStrategy = debounce(
|
||||
@@ -474,10 +420,11 @@ class PanelHome extends SubscribeMixin(LitElement) {
|
||||
return {
|
||||
strategy: {
|
||||
type: "home",
|
||||
alert_entities: this._securityConfig.alert_entities,
|
||||
security_hidden_entities: this._securityConfig.hidden_entities,
|
||||
maintenance_hidden_entities: this._maintenanceConfig.hidden_entities,
|
||||
climate_hidden_entities: this._climateConfig.hidden_entities,
|
||||
alert_entities: this._summaryConfigs.security.alert_entities,
|
||||
security_hidden_entities: this._summaryConfigs.security.hidden_entities,
|
||||
maintenance_hidden_entities:
|
||||
this._summaryConfigs.maintenance.hidden_entities,
|
||||
climate_hidden_entities: this._summaryConfigs.climate.hidden_entities,
|
||||
favorite_entities: this._config?.favorite_entities,
|
||||
home_panel: true,
|
||||
hide_welcome_message: this._config?.hide_welcome_message,
|
||||
|
||||
@@ -1,313 +1,9 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import {
|
||||
fetchFrontendSystemDataAfterSaves,
|
||||
queueFrontendSystemDataSave,
|
||||
type MaintenanceFrontendSystemData,
|
||||
} from "../../data/frontend";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { ChildPanelReady } from "../../layouts/panel-ready";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-background";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
|
||||
const MAINTENANCE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
type: "maintenance",
|
||||
},
|
||||
};
|
||||
import { customElement } from "lit/decorators";
|
||||
import { HaHomeSummaryPanelBase } from "../home/ha-home-summary-panel-base";
|
||||
|
||||
@customElement("ha-panel-maintenance")
|
||||
class PanelMaintenance extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state() private _viewIndex = 0;
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _config?: MaintenanceFrontendSystemData;
|
||||
|
||||
@state() private _editVisibility = false;
|
||||
|
||||
@state() private _searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
private _childPanelReady?: ChildPanelReady;
|
||||
|
||||
private _loadConfigPromise?: Promise<void>;
|
||||
|
||||
private _loadConfigRevision = 0;
|
||||
|
||||
private _setLovelaceRevision = 0;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (
|
||||
oldHass &&
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && this.hass) {
|
||||
// Refresh the generated view when registries or panels change.
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
await Promise.all([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
this._loadConfigPromise,
|
||||
]);
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
const revision = ++this._loadConfigRevision;
|
||||
this._config = undefined;
|
||||
try {
|
||||
const data = await fetchFrontendSystemDataAfterSaves(
|
||||
this.hass.connection,
|
||||
"maintenance"
|
||||
);
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
this._config = data || {};
|
||||
} catch (err) {
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
this._editVisibility = false;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load maintenance configuration:", err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.maintenance.editor.load_failed"),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
);
|
||||
|
||||
private _registriesChanged = async () => {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${this._searchParams.has("historyBack")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.maintenance")}</div>
|
||||
${
|
||||
!this.hass.user?.is_admin
|
||||
? nothing
|
||||
: this._editVisibility
|
||||
? html`<ha-button
|
||||
slot="actionItems"
|
||||
appearance="filled"
|
||||
size="s"
|
||||
@click=${this._stopEditVisibility}
|
||||
>
|
||||
${this.hass.localize("ui.panel.lovelace.menu.exit_edit_mode")}
|
||||
</ha-button>`
|
||||
: this._config
|
||||
? html`<ha-icon-button
|
||||
slot="actionItems"
|
||||
.path=${mdiPencil}
|
||||
.label=${this.hass.localize("ui.panel.maintenance.editor.title")}
|
||||
@click=${this._startEditVisibility}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}>
|
||||
</hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
@entity-visibility-changed=${this._visibilityChanged}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLovelace(force = false) {
|
||||
const revision = ++this._setLovelaceRevision;
|
||||
if (this._loadConfigPromise) {
|
||||
await this._loadConfigPromise;
|
||||
}
|
||||
|
||||
const viewConfig = await generateLovelaceViewStrategy(
|
||||
{
|
||||
strategy: {
|
||||
...MAINTENANCE_LOVELACE_VIEW_CONFIG.strategy,
|
||||
hidden_entities: this._config?.hidden_entities,
|
||||
edit_visibility: this._editVisibility || undefined,
|
||||
},
|
||||
},
|
||||
this.hass
|
||||
);
|
||||
|
||||
if (revision !== this._setLovelaceRevision) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { views: [viewConfig] };
|
||||
const rawConfig = { views: [MAINTENANCE_LOVELACE_VIEW_CONFIG] };
|
||||
|
||||
if (!force && deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._childPanelReady ??= new ChildPanelReady(this);
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
editMode: false,
|
||||
urlPath: "maintenance",
|
||||
mode: "generated",
|
||||
locale: this.hass.locale,
|
||||
enableFullEditMode: () => undefined,
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
showToast: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private _startEditVisibility = () => {
|
||||
this._editVisibility = true;
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _stopEditVisibility = () => {
|
||||
this._editVisibility = false;
|
||||
if (!this._config) {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
}
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _visibilityChanged = (
|
||||
ev: HASSDomEvent<HASSDomEvents["entity-visibility-changed"]>
|
||||
) => {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const { entity, hidden } = ev.detail;
|
||||
const hiddenEntities = (this._config.hidden_entities ?? []).filter(
|
||||
(entityId) => entityId !== entity
|
||||
);
|
||||
if (hidden) {
|
||||
hiddenEntities.push(entity);
|
||||
}
|
||||
this._saveConfig({
|
||||
...this._config,
|
||||
hidden_entities: hiddenEntities.length ? hiddenEntities : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
private _saveConfig(config: MaintenanceFrontendSystemData): Promise<void> {
|
||||
this._config = config;
|
||||
return queueFrontendSystemDataSave(
|
||||
this.hass.connection,
|
||||
"maintenance",
|
||||
config
|
||||
).catch(async (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to save maintenance configuration:", err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.maintenance.editor.save_failed"),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
if (config !== this._config) {
|
||||
return;
|
||||
}
|
||||
await this._loadConfig();
|
||||
await this._setLovelace(true);
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
user-select: none;
|
||||
}
|
||||
hui-view-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
hui-view {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
class PanelMaintenance extends HaHomeSummaryPanelBase<"maintenance"> {
|
||||
protected readonly _systemDataKey = "maintenance";
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,288 +1,21 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import type { HaButton } from "../../components/ha-button";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import {
|
||||
fetchFrontendSystemDataAfterSaves,
|
||||
queueFrontendSystemDataSave,
|
||||
type SecurityFrontendSystemData,
|
||||
} from "../../data/frontend";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { ChildPanelReady } from "../../layouts/panel-ready";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import { clearEntityVisibilityFocus } from "../lovelace/cards/hui-entity-visibility-card";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
import "../lovelace/views/hui-view-background";
|
||||
|
||||
const SECURITY_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
type: "security",
|
||||
},
|
||||
};
|
||||
import { HaHomeSummaryPanelBase } from "../home/ha-home-summary-panel-base";
|
||||
|
||||
@customElement("ha-panel-security")
|
||||
class PanelSecurity extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
class PanelSecurity extends HaHomeSummaryPanelBase<"security"> {
|
||||
protected readonly _systemDataKey = "security";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state() private _viewIndex = 0;
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _config?: SecurityFrontendSystemData;
|
||||
|
||||
@state() private _editVisibility = false;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@query("ha-button") private _exitEditButton?: HaButton;
|
||||
|
||||
private _childPanelReady?: ChildPanelReady;
|
||||
|
||||
private _loadConfigPromise?: Promise<void>;
|
||||
|
||||
private _loadConfigRevision = 0;
|
||||
|
||||
private _setLovelaceRevision = 0;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (
|
||||
oldHass &&
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && this.hass) {
|
||||
// Refresh the generated view when registries or panels change.
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
await Promise.all([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
this._loadConfigPromise,
|
||||
]);
|
||||
await this._setLovelace();
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
const revision = ++this._loadConfigRevision;
|
||||
this._config = undefined;
|
||||
try {
|
||||
const data = await fetchFrontendSystemDataAfterSaves(
|
||||
this.hass.connection,
|
||||
"security"
|
||||
);
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
this._config = data || {};
|
||||
} catch (err) {
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load security configuration:", err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.security.editor.load_failed"),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
);
|
||||
|
||||
private _registriesChanged = async () => {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${this._searchParms.has("historyBack")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.security")}</div>
|
||||
${
|
||||
!this.hass.user?.is_admin
|
||||
? nothing
|
||||
: this._editVisibility
|
||||
? html`<ha-button
|
||||
slot="actionItems"
|
||||
appearance="filled"
|
||||
size="s"
|
||||
@click=${this._stopEditVisibility}
|
||||
>
|
||||
${this.hass.localize("ui.panel.lovelace.menu.exit_edit_mode")}
|
||||
</ha-button>`
|
||||
: this._config
|
||||
? html`<ha-icon-button
|
||||
slot="actionItems"
|
||||
.path=${mdiPencil}
|
||||
.label=${this.hass.localize("ui.panel.security.editor.title")}
|
||||
@click=${this._startEditVisibility}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}>
|
||||
</hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
@entity-visibility-changed=${this._visibilityChanged}
|
||||
@entity-favorite-changed=${this._favoriteChanged}
|
||||
@entity-alert-changed=${this._alertChanged}
|
||||
@entity-visibility-focus-lost=${this._focusLost}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLovelace(force = false) {
|
||||
const revision = ++this._setLovelaceRevision;
|
||||
if (this._loadConfigPromise) {
|
||||
await this._loadConfigPromise;
|
||||
}
|
||||
|
||||
const viewConfig = await generateLovelaceViewStrategy(
|
||||
{
|
||||
strategy: {
|
||||
...SECURITY_LOVELACE_VIEW_CONFIG.strategy,
|
||||
alert_entities: this._config?.alert_entities,
|
||||
favorite_entities: this._config?.favorite_entities,
|
||||
hidden_entities: this._config?.hidden_entities,
|
||||
edit_visibility: this._editVisibility || undefined,
|
||||
},
|
||||
},
|
||||
this.hass
|
||||
);
|
||||
|
||||
if (revision !== this._setLovelaceRevision) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { views: [viewConfig] };
|
||||
const rawConfig = { views: [SECURITY_LOVELACE_VIEW_CONFIG] };
|
||||
|
||||
if (!force && deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._childPanelReady ??= new ChildPanelReady(this);
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
editMode: false,
|
||||
urlPath: "security",
|
||||
mode: "generated",
|
||||
locale: this.hass.locale,
|
||||
enableFullEditMode: () => undefined,
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
showToast: () => undefined,
|
||||
protected _strategyOptions() {
|
||||
return {
|
||||
alert_entities: this._config?.alert_entities,
|
||||
favorite_entities: this._config?.favorite_entities,
|
||||
};
|
||||
}
|
||||
|
||||
private _startEditVisibility = () => {
|
||||
clearEntityVisibilityFocus();
|
||||
this._editVisibility = true;
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _stopEditVisibility = () => {
|
||||
this._editVisibility = false;
|
||||
if (!this._config) {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
}
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _focusLost = () => {
|
||||
this._exitEditButton?.focus();
|
||||
};
|
||||
|
||||
private _visibilityChanged = (
|
||||
ev: HASSDomEvent<HASSDomEvents["entity-visibility-changed"]>
|
||||
) => {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const { entity, hidden } = ev.detail;
|
||||
const hiddenEntities = (this._config.hidden_entities ?? []).filter(
|
||||
(entityId) => entityId !== entity
|
||||
);
|
||||
if (hidden) {
|
||||
hiddenEntities.push(entity);
|
||||
}
|
||||
this._saveConfig({
|
||||
...this._config,
|
||||
hidden_entities: hiddenEntities.length ? hiddenEntities : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
private _favoriteChanged = (
|
||||
protected _favoriteChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["entity-favorite-changed"]>
|
||||
) => {
|
||||
) {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
@@ -298,11 +31,11 @@ class PanelSecurity extends LitElement {
|
||||
favorite_entities: favoriteEntities.length ? favoriteEntities : undefined,
|
||||
});
|
||||
this._setLovelace();
|
||||
};
|
||||
}
|
||||
|
||||
private _alertChanged = (
|
||||
protected _alertChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["entity-alert-changed"]>
|
||||
) => {
|
||||
) {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
@@ -322,48 +55,6 @@ class PanelSecurity extends LitElement {
|
||||
alert_entities: alertEntities.length ? alertEntities : undefined,
|
||||
});
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _saveConfig(config: SecurityFrontendSystemData): Promise<void> {
|
||||
this._config = config;
|
||||
return queueFrontendSystemDataSave(
|
||||
this.hass.connection,
|
||||
"security",
|
||||
config
|
||||
).catch(async (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to save security configuration:", err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.security.editor.save_failed"),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
if (config !== this._config) {
|
||||
return;
|
||||
}
|
||||
await this._loadConfig();
|
||||
await this._setLovelace(true);
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
user-select: none;
|
||||
}
|
||||
hui-view-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
hui-view {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user