- Text-to-Speech
+ Text-to-speech
${renderProgress(this.hass, this.pipelineRun, "tts")}
${this.pipelineRun.tts
diff --git a/src/panels/developer-tools/service/developer-tools-service.ts b/src/panels/developer-tools/service/developer-tools-service.ts
index a169b0fb27..6096bd16b4 100644
--- a/src/panels/developer-tools/service/developer-tools-service.ts
+++ b/src/panels/developer-tools/service/developer-tools-service.ts
@@ -4,7 +4,7 @@ import { load } from "js-yaml";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
-import { LocalStorage } from "../../../common/decorators/local-storage";
+import { storage } from "../../../common/decorators/storage";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { hasTemplate } from "../../../common/string/has-template";
@@ -38,10 +38,18 @@ class HaPanelDevService extends LitElement {
@state() private _uiAvailable = true;
- @LocalStorage("panel-dev-service-state-service-data", true, false)
+ @storage({
+ key: "panel-dev-service-state-service-data",
+ state: true,
+ subscribe: false,
+ })
private _serviceData?: ServiceAction = { service: "", target: {}, data: {} };
- @LocalStorage("panel-dev-service-state-yaml-mode", true, false)
+ @storage({
+ key: "panel-dev-service-state-yaml-mode",
+ state: true,
+ subscribe: false,
+ })
private _yamlMode = false;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts
index bb3c224750..94d5ab6c15 100644
--- a/src/panels/history/ha-panel-history.ts
+++ b/src/panels/history/ha-panel-history.ts
@@ -7,7 +7,7 @@ import {
import { css, html, LitElement, PropertyValues } from "lit";
import { property, query, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
-import { LocalStorage } from "../../common/decorators/local-storage";
+import { storage } from "../../common/decorators/storage";
import { navigate } from "../../common/navigate";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
import {
@@ -58,7 +58,11 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
@state() private _endDate: Date;
- @LocalStorage("historyPickedValue", true, false)
+ @storage({
+ key: "historyPickedValue",
+ state: true,
+ subscribe: false,
+ })
private _targetPickerValue?: HassServiceTarget;
@state() private _isLoading = false;
diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts
index 548dbd80ec..7424877f52 100644
--- a/src/panels/lovelace/cards/hui-entity-card.ts
+++ b/src/panels/lovelace/cards/hui-entity-card.ts
@@ -4,14 +4,15 @@ import {
CSSResultGroup,
html,
LitElement,
- PropertyValues,
nothing,
+ PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
+import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@@ -27,7 +28,6 @@ import "../../../components/ha-card";
import "../../../components/ha-icon";
import { HVAC_ACTION_TO_MODE } from "../../../data/climate";
import { isUnavailableState } from "../../../data/entity";
-import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { LightEntity } from "../../../data/light";
import { HomeAssistant } from "../../../types";
import { computeCardSize } from "../common/compute-card-size";
@@ -35,21 +35,12 @@ import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
-import {
- LovelaceCard,
- LovelaceCardEditor,
- LovelaceHeaderFooter,
-} from "../types";
+import { LovelaceCard, LovelaceHeaderFooter } from "../types";
import { HuiErrorCard } from "./hui-error-card";
import { EntityCardConfig } from "./types";
@customElement("hui-entity-card")
export class HuiEntityCard extends LitElement implements LovelaceCard {
- public static async getConfigElement(): Promise
{
- await import("../editor/config-elements/hui-entity-card-editor");
- return document.createElement("hui-entity-card-editor");
- }
-
public static getStubConfig(
hass: HomeAssistant,
entities: string[],
@@ -70,6 +61,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
};
}
+ public static async getConfigForm() {
+ return (await import("../editor/config-elements/hui-entity-card-editor"))
+ .default;
+ }
+
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EntityCardConfig;
diff --git a/src/panels/lovelace/cards/hui-picture-card.ts b/src/panels/lovelace/cards/hui-picture-card.ts
index c72bf72bc9..843d447a67 100644
--- a/src/panels/lovelace/cards/hui-picture-card.ts
+++ b/src/panels/lovelace/cards/hui-picture-card.ts
@@ -3,19 +3,22 @@ import {
CSSResultGroup,
html,
LitElement,
- PropertyValues,
nothing,
+ PropertyValues,
} from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import "../../../components/ha-card";
+import { computeImageUrl, ImageEntity } from "../../../data/image";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
+import { hasConfigChanged } from "../common/has-changed";
+import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { PictureCardConfig } from "./types";
@@ -30,8 +33,6 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
return {
type: "picture",
image: "https://demo.home-assistant.io/stub_config/t-shirt-promo.png",
- tap_action: { action: "none" },
- hold_action: { action: "none" },
};
}
@@ -44,7 +45,7 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
}
public setConfig(config: PictureCardConfig): void {
- if (!config || !config.image) {
+ if (!config || (!config.image && !config.image_entity)) {
throw new Error("Image required");
}
@@ -52,10 +53,21 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
- if (changedProps.size === 1 && changedProps.has("hass")) {
- return !changedProps.get("hass");
+ if (!this._config || hasConfigChanged(this, changedProps)) {
+ return true;
}
- return true;
+ if (this._config.image_entity && changedProps.has("hass")) {
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
+ if (
+ !oldHass ||
+ oldHass.states[this._config.image_entity] !==
+ this.hass!.states[this._config.image_entity]
+ ) {
+ return true;
+ }
+ }
+
+ return false;
}
protected updated(changedProps: PropertyValues): void {
@@ -83,6 +95,17 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
return nothing;
}
+ let stateObj: ImageEntity | undefined;
+
+ if (this._config.image_entity) {
+ stateObj = this.hass.states[this._config.image_entity] as ImageEntity;
+ if (!stateObj) {
+ return html`
+ ${createEntityNotFoundWarning(this.hass, this._config.image_entity)}
+ `;
+ }
+ }
+
return html`
`;
diff --git a/src/panels/lovelace/cards/hui-picture-elements-card.ts b/src/panels/lovelace/cards/hui-picture-elements-card.ts
index fad22f2c66..15c7f02e1f 100644
--- a/src/panels/lovelace/cards/hui-picture-elements-card.ts
+++ b/src/panels/lovelace/cards/hui-picture-elements-card.ts
@@ -9,6 +9,7 @@ import {
import { customElement, property, state } from "lit/decorators";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import "../../../components/ha-card";
+import { ImageEntity, computeImageUrl } from "../../../data/image";
import { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities";
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
@@ -62,7 +63,12 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
if (!config) {
throw new Error("Invalid configuration");
} else if (
- !(config.image || config.camera_image || config.state_image) ||
+ !(
+ config.image ||
+ config.image_entity ||
+ config.camera_image ||
+ config.state_image
+ ) ||
(config.state_image && !config.entity)
) {
throw new Error("Image required");
@@ -115,12 +121,17 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
return nothing;
}
+ let stateObj: ImageEntity | undefined;
+ if (this._config.image_entity) {
+ stateObj = this.hass.states[this._config.image_entity] as ImageEntity;
+ }
+
return html`
${entityState}
`;
}
+ const domain = computeDomain(this._config.entity);
+
return html`
${this._config.title
- ? html`
${this._config.title}
`
+ ? html`
${this._config.title}
`
: ""}
${this._entitiesDialog!.map((entityConf) =>
diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts
index fac076ad93..082b6c4b53 100644
--- a/src/panels/lovelace/cards/types.ts
+++ b/src/panels/lovelace/cards/types.ts
@@ -335,6 +335,7 @@ export interface StatisticCardConfig extends LovelaceCardConfig {
export interface PictureCardConfig extends LovelaceCardConfig {
image?: string;
+ image_entity?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
@@ -345,6 +346,7 @@ export interface PictureCardConfig extends LovelaceCardConfig {
export interface PictureElementsCardConfig extends LovelaceCardConfig {
title?: string;
image?: string;
+ image_entity?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];
state_image?: Record
;
diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts
index 99bfbdc634..65b66c3ce1 100644
--- a/src/panels/lovelace/common/generate-lovelace-config.ts
+++ b/src/panels/lovelace/common/generate-lovelace-config.ts
@@ -20,6 +20,7 @@ import {
AlarmPanelCardConfig,
EntitiesCardConfig,
HumidifierCardConfig,
+ PictureCardConfig,
PictureEntityCardConfig,
ThermostatCardConfig,
} from "../cards/types";
@@ -125,6 +126,12 @@ export const computeCards = (
entity: entityId,
};
cards.push(cardConfig);
+ } else if (domain === "image") {
+ const cardConfig: PictureCardConfig = {
+ type: "picture",
+ image_entity: entityId,
+ };
+ cards.push(cardConfig);
} else if (domain === "climate") {
const cardConfig: ThermostatCardConfig = {
type: "thermostat",
diff --git a/src/panels/lovelace/common/handle-action.ts b/src/panels/lovelace/common/handle-action.ts
index f782d6ad53..83deadf5db 100644
--- a/src/panels/lovelace/common/handle-action.ts
+++ b/src/panels/lovelace/common/handle-action.ts
@@ -18,6 +18,7 @@ declare global {
export type ActionConfigParams = {
entity?: string;
camera_image?: string;
+ image_entity?: string;
hold_action?: ActionConfig;
tap_action?: ActionConfig;
double_tap_action?: ActionConfig;
@@ -87,9 +88,11 @@ export const handleAction = async (
switch (actionConfig.action) {
case "more-info": {
- if (config.entity || config.camera_image) {
+ if (config.entity || config.camera_image || config.image_entity) {
fireEvent(node, "hass-more-info", {
- entityId: config.entity ? config.entity : config.camera_image!,
+ entityId: (config.entity ||
+ config.camera_image ||
+ config.image_entity)!,
});
} else {
showToast(node, {
diff --git a/src/panels/lovelace/components/hui-image.ts b/src/panels/lovelace/components/hui-image.ts
index 56c0873608..33201fd7af 100644
--- a/src/panels/lovelace/components/hui-image.ts
+++ b/src/panels/lovelace/components/hui-image.ts
@@ -10,12 +10,14 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { STATES_OFF } from "../../../common/const";
+import { computeDomain } from "../../../common/entity/compute_domain";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
import "../../../components/ha-camera-stream";
import type { HaCameraStream } from "../../../components/ha-camera-stream";
import "../../../components/ha-circular-progress";
import { CameraEntity, fetchThumbnailUrlWithCache } from "../../../data/camera";
import { UNAVAILABLE } from "../../../data/entity";
+import { computeImageUrl, ImageEntity } from "../../../data/image";
import { HomeAssistant } from "../../../types";
const UPDATE_INTERVAL = 10000;
@@ -164,6 +166,8 @@ export class HuiImage extends LitElement {
}
} else if (this.darkModeImage && this.hass.themes.darkMode) {
imageSrc = this.darkModeImage;
+ } else if (stateObj && computeDomain(stateObj.entity_id) === "image") {
+ imageSrc = computeImageUrl(stateObj as ImageEntity);
} else {
imageSrc = this.image;
}
diff --git a/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts
index 08a0b9bd42..be036fb404 100644
--- a/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts
@@ -1,7 +1,7 @@
import { customElement } from "lit/decorators";
import type { LovelaceCardConfig } from "../../../../data/lovelace";
import { getCardElementClass } from "../../create-element/create-card-element";
-import type { LovelaceCardEditor } from "../../types";
+import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
import { HuiElementEditor } from "../hui-element-editor";
@customElement("hui-card-element-editor")
@@ -16,6 +16,17 @@ export class HuiCardElementEditor extends HuiElementEditor {
return undefined;
}
+
+ protected async getConfigForm(): Promise {
+ const elClass = await getCardElementClass(this.configElementType!);
+
+ // Check if a schema exists
+ if (elClass && elClass.getConfigForm) {
+ return elClass.getConfigForm();
+ }
+
+ return undefined;
+ }
}
declare global {
diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts
index 3379792b6f..4c93882620 100644
--- a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts
@@ -1,16 +1,12 @@
-import { html, LitElement, nothing } from "lit";
-import { customElement, property, state } from "lit/decorators";
import { assert, assign, boolean, object, optional, string } from "superstruct";
-import { fireEvent } from "../../../../common/dom/fire_event";
-import "../../../../components/ha-form/ha-form";
-import type { SchemaUnion } from "../../../../components/ha-form/types";
-import type { HomeAssistant } from "../../../../types";
-import type { EntityCardConfig } from "../../cards/types";
+import { LocalizeFunc } from "../../../../common/translations/localize";
+import { HaFormSchema } from "../../../../components/ha-form/types";
+import { EntityCardConfig } from "../../cards/types";
import { headerFooterConfigStructs } from "../../header-footer/structs";
-import type { LovelaceCardEditor } from "../../types";
+import { LovelaceConfigForm } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
-const cardConfigStruct = assign(
+const struct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
@@ -54,67 +50,19 @@ const SCHEMA = [
{ name: "state_color", selector: { boolean: {} } },
],
},
-] as const;
-
-@customElement("hui-entity-card-editor")
-export class HuiEntityCardEditor
- extends LitElement
- implements LovelaceCardEditor
-{
- @property({ attribute: false }) public hass?: HomeAssistant;
-
- @state() private _config?: EntityCardConfig;
-
- public setConfig(config: EntityCardConfig): void {
- assert(config, cardConfigStruct);
- this._config = config;
- }
-
- protected render() {
- if (!this.hass || !this._config) {
- return nothing;
- }
-
- return html`
-
- `;
- }
-
- private _valueChanged(ev: CustomEvent): void {
- const config = ev.detail.value;
- Object.keys(config).forEach((k) => config[k] === "" && delete config[k]);
- fireEvent(this, "config-changed", { config });
- }
-
- private _computeLabelCallback = (schema: SchemaUnion) => {
- if (schema.name === "entity") {
- return this.hass!.localize(
- "ui.panel.lovelace.editor.card.generic.entity"
- );
- }
+] as HaFormSchema[];
+const entityCardConfigForm: LovelaceConfigForm = {
+ schema: SCHEMA,
+ assertConfig: (config: EntityCardConfig) => assert(config, struct),
+ computeLabel: (schema: HaFormSchema, localize: LocalizeFunc) => {
if (schema.name === "theme") {
- return `${this.hass!.localize(
+ return `${localize(
"ui.panel.lovelace.editor.card.generic.theme"
- )} (${this.hass!.localize(
- "ui.panel.lovelace.editor.card.config.optional"
- )})`;
+ )} (${localize("ui.panel.lovelace.editor.card.config.optional")})`;
}
+ return localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
+ },
+};
- return this.hass!.localize(
- `ui.panel.lovelace.editor.card.generic.${schema.name}`
- );
- };
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "hui-entity-card-editor": HuiEntityCardEditor;
- }
-}
+export default entityCardConfigForm;
diff --git a/src/panels/lovelace/editor/config-elements/hui-form-editor.ts b/src/panels/lovelace/editor/config-elements/hui-form-editor.ts
new file mode 100644
index 0000000000..9db046adfe
--- /dev/null
+++ b/src/panels/lovelace/editor/config-elements/hui-form-editor.ts
@@ -0,0 +1,82 @@
+import { CSSResultGroup, html, LitElement, nothing } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { fireEvent } from "../../../../common/dom/fire_event";
+import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
+import { LocalizeFunc } from "../../../../common/translations/localize";
+import "../../../../components/ha-form/ha-form";
+import type { HaFormSchema } from "../../../../components/ha-form/types";
+import { LovelaceCardConfig } from "../../../../data/lovelace";
+import type { HomeAssistant } from "../../../../types";
+import type { LovelaceGenericElementEditor } from "../../types";
+import { configElementStyle } from "./config-elements-style";
+
+@customElement("hui-form-editor")
+export class HuiFormEditor
+ extends LitElement
+ implements LovelaceGenericElementEditor
+{
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public schema!: HaFormSchema[];
+
+ @state() private _config?: LovelaceCardConfig;
+
+ public assertConfig(_config: LovelaceCardConfig): void {
+ return undefined;
+ }
+
+ public setConfig(config: LovelaceCardConfig): void {
+ this.assertConfig(config);
+ this._config = config;
+ }
+
+ protected render() {
+ if (!this._config) {
+ return nothing;
+ }
+
+ return html`
+
+ `;
+ }
+
+ public computeLabel = (
+ _schema: HaFormSchema,
+ _localize: LocalizeFunc
+ ): string | undefined => undefined;
+
+ public computeHelper = (
+ _schema: HaFormSchema,
+ _localize: LocalizeFunc
+ ): string | undefined => undefined;
+
+ private _computeLabelCallback = (schema: HaFormSchema) =>
+ this.computeLabel(schema, this.hass.localize) ||
+ this.hass.localize(
+ `ui.panel.lovelace.editor.card.generic.${schema.name}`
+ ) ||
+ capitalizeFirstLetter(schema.name.split("_").join(" "));
+
+ private _computeHelperCallback = (schema: HaFormSchema) =>
+ this.computeHelper(schema, this.hass.localize);
+
+ private _valueChanged(ev: CustomEvent): void {
+ const config = ev.detail.value;
+ fireEvent(this, "config-changed", { config });
+ }
+
+ static styles: CSSResultGroup = configElementStyle;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-form-editor": HuiFormEditor;
+ }
+}
diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts
index 29e001688d..1ce62d6a78 100644
--- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts
@@ -1,22 +1,21 @@
-import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
+import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
+import { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-theme-picker";
-import { ActionConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { PictureCardConfig } from "../../cards/types";
import "../../components/hui-action-editor";
import { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
-import { EditorTarget } from "../types";
-import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
image: optional(string()),
+ image_entity: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
theme: optional(string()),
@@ -24,6 +23,21 @@ const cardConfigStruct = assign(
})
);
+const SCHEMA = [
+ { name: "image", selector: { text: {} } },
+ { name: "image_entity", selector: { entity: { domain: "image" } } },
+ { name: "alt_text", selector: { text: {} } },
+ { name: "theme", selector: { theme: {} } },
+ {
+ name: "tap_action",
+ selector: { ui_action: {} },
+ },
+ {
+ name: "hold_action",
+ selector: { ui_action: {} },
+ },
+] as const;
+
@customElement("hui-picture-card-editor")
export class HuiPictureCardEditor
extends LitElement
@@ -38,129 +52,45 @@ export class HuiPictureCardEditor
this._config = config;
}
- get _image(): string {
- return this._config!.image || "";
- }
-
- get _tap_action(): ActionConfig {
- return this._config!.tap_action || { action: "none" };
- }
-
- get _hold_action(): ActionConfig {
- return this._config!.hold_action || { action: "none" };
- }
-
- get _theme(): string {
- return this._config!.theme || "";
- }
-
- get _alt_text(): string {
- return this._config!.alt_text || "";
- }
-
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
- const actions = ["navigate", "url", "call-service", "none"];
-
return html`
-
-
-
-
-
-
-
+
`;
}
private _valueChanged(ev: CustomEvent): void {
- if (!this._config || !this.hass) {
- return;
- }
- const target = ev.target! as EditorTarget;
- const value = ev.detail?.value ?? target.value;
-
- if (this[`_${target.configValue}`] === value) {
- return;
- }
- if (target.configValue) {
- if (value !== false && !value) {
- this._config = { ...this._config };
- delete this._config[target.configValue!];
- } else {
- this._config = {
- ...this._config,
- [target.configValue!]: value,
- };
- }
- }
- fireEvent(this, "config-changed", { config: this._config });
+ fireEvent(this, "config-changed", { config: ev.detail.value });
}
- static get styles(): CSSResultGroup {
- return [
- configElementStyle,
- css`
- ha-textfield {
- display: block;
- margin-bottom: 8px;
- }
- `,
- ];
- }
+ private _computeLabelCallback = (schema: SchemaUnion) => {
+ switch (schema.name) {
+ case "theme":
+ return `${this.hass!.localize(
+ "ui.panel.lovelace.editor.card.generic.theme"
+ )} (${this.hass!.localize(
+ "ui.panel.lovelace.editor.card.config.optional"
+ )})`;
+ default:
+ return (
+ this.hass!.localize(
+ `ui.panel.lovelace.editor.card.picture-card.${schema.name}`
+ ) ||
+ this.hass!.localize(
+ `ui.panel.lovelace.editor.card.generic.${schema.name}`
+ )
+ );
+ }
+ };
}
declare global {
diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts
index 76c0634698..c6c3d12146 100644
--- a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts
@@ -22,6 +22,7 @@ const cardConfigStruct = assign(
title: optional(string()),
entity: optional(string()),
image: optional(string()),
+ image_entity: optional(string()),
camera_image: optional(string()),
camera_view: optional(string()),
aspect_ratio: optional(string()),
@@ -35,6 +36,7 @@ const cardConfigStruct = assign(
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{ name: "image", selector: { text: {} } },
+ { name: "image_entity", selector: { entity: { domain: "image" } } },
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{
name: "",
diff --git a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts
index e16a629b8f..8cb52183c5 100644
--- a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts
@@ -187,7 +187,7 @@ export class HuiStatisticsGraphCardEditor
),
disabled:
!metaDatas ||
- !metaDatas.every((metaData) =>
+ !metaDatas.some((metaData) =>
statisticsMetaHasType(
metaData,
supportedStatTypeMap[stat_type]
@@ -246,12 +246,10 @@ export class HuiStatisticsGraphCardEditor
);
const configured_stat_types = this._config!.stat_types
? ensureArray(this._config.stat_types)
- : stat_types.filter(
- (stat_type) =>
- stat_type !== "change" &&
- this._metaDatas?.every((metaData) =>
- statisticsMetaHasType(metaData, stat_type)
- )
+ : stat_types.filter((stat_type) =>
+ this._metaDatas?.some((metaData) =>
+ statisticsMetaHasType(metaData, stat_type)
+ )
);
const data = {
chart_type: "line",
@@ -320,9 +318,7 @@ export class HuiStatisticsGraphCardEditor
: undefined;
if (config.stat_types && config.entities.length) {
config.stat_types = ensureArray(config.stat_types).filter((stat_type) =>
- metadata!.every((metaData) =>
- statisticsMetaHasType(metaData, stat_type)
- )
+ metadata!.some((metaData) => statisticsMetaHasType(metaData, stat_type))
);
if (!config.stat_types.length) {
delete config.stat_types;
diff --git a/src/panels/lovelace/editor/hui-element-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts
index b7b2dbf324..f711c63210 100644
--- a/src/panels/lovelace/editor/hui-element-editor.ts
+++ b/src/panels/lovelace/editor/hui-element-editor.ts
@@ -8,13 +8,13 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
-import { property, state, query } from "lit/decorators";
+import { property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { handleStructError } from "../../../common/structs/handle-errors";
import { deepEqual } from "../../../common/util/deep-equal";
+import "../../../components/ha-alert";
import "../../../components/ha-circular-progress";
import "../../../components/ha-code-editor";
-import "../../../components/ha-alert";
import type { HaCodeEditor } from "../../../components/ha-code-editor";
import type {
LovelaceCardConfig,
@@ -23,11 +23,15 @@ import type {
import type { HomeAssistant } from "../../../types";
import type { LovelaceRowConfig } from "../entity-rows/types";
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
-import type { LovelaceGenericElementEditor } from "../types";
+import { LovelaceTileFeatureConfig } from "../tile-features/types";
+import type {
+ LovelaceConfigForm,
+ LovelaceGenericElementEditor,
+} from "../types";
+import type { HuiFormEditor } from "./config-elements/hui-form-editor";
import "./config-elements/hui-generic-entity-row-editor";
import { GUISupportError } from "./gui-support-error";
import { EditSubElementEvent, GUIModeChangedEvent } from "./types";
-import { LovelaceTileFeatureConfig } from "../tile-features/types";
export interface ConfigChangedEvent {
config:
@@ -182,6 +186,10 @@ export abstract class HuiElementEditor extends LitElement {
return undefined;
}
+ protected async getConfigForm(): Promise {
+ return undefined;
+ }
+
protected get configElementType(): string | undefined {
return this.value ? (this.value as any).type : undefined;
}
@@ -328,6 +336,25 @@ export abstract class HuiElementEditor extends LitElement {
this._loading = true;
configElement = await this.getConfigElement();
+ if (!configElement) {
+ const form = await this.getConfigForm();
+ if (form) {
+ await import("./config-elements/hui-form-editor");
+ configElement = document.createElement("hui-form-editor");
+ const { schema, assertConfig, computeLabel, computeHelper } = form;
+ (configElement as HuiFormEditor).schema = schema;
+ if (computeLabel) {
+ (configElement as HuiFormEditor).computeLabel = computeLabel;
+ }
+ if (computeHelper) {
+ (configElement as HuiFormEditor).computeHelper = computeHelper;
+ }
+ if (assertConfig) {
+ (configElement as HuiFormEditor).assertConfig = assertConfig;
+ }
+ }
+ }
+
if (configElement) {
configElement.hass = this.hass;
if ("lovelace" in configElement) {
diff --git a/src/panels/lovelace/editor/tile-feature-editor/hui-tile-feature-element-editor.ts b/src/panels/lovelace/editor/tile-feature-editor/hui-tile-feature-element-editor.ts
index 573d1e075c..58fb381399 100644
--- a/src/panels/lovelace/editor/tile-feature-editor/hui-tile-feature-element-editor.ts
+++ b/src/panels/lovelace/editor/tile-feature-editor/hui-tile-feature-element-editor.ts
@@ -4,7 +4,10 @@ import {
LovelaceTileFeatureConfig,
LovelaceTileFeatureContext,
} from "../../tile-features/types";
-import type { LovelaceTileFeatureEditor } from "../../types";
+import type {
+ LovelaceConfigForm,
+ LovelaceTileFeatureEditor,
+} from "../../types";
import { HuiElementEditor } from "../hui-element-editor";
@customElement("hui-tile-feature-element-editor")
@@ -24,6 +27,17 @@ export class HuiTileFeatureElementEditor extends HuiElementEditor<
return undefined;
}
+
+ protected async getConfigForm(): Promise {
+ const elClass = await getTileFeatureElementClass(this.configElementType!);
+
+ // Check if a schema exists
+ if (elClass && elClass.getConfigForm) {
+ return elClass.getConfigForm();
+ }
+
+ return undefined;
+ }
}
declare global {
diff --git a/src/panels/lovelace/elements/hui-image-element.ts b/src/panels/lovelace/elements/hui-image-element.ts
index e54e23fb68..e86058a183 100644
--- a/src/panels/lovelace/elements/hui-image-element.ts
+++ b/src/panels/lovelace/elements/hui-image-element.ts
@@ -1,6 +1,7 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
+import { ImageEntity, computeImageUrl } from "../../../data/image";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { computeTooltip } from "../common/compute-tooltip";
@@ -34,12 +35,16 @@ export class HuiImageElement extends LitElement implements LovelaceElement {
if (!this._config || !this.hass) {
return nothing;
}
+ let stateObj: ImageEntity | undefined;
+ if (this._config.image_entity) {
+ stateObj = this.hass.states[this._config.image_entity] as ImageEntity;
+ }
return html`
void;
+ computeLabel?: (
+ schema: HaFormSchema,
+ localize: LocalizeFunc
+ ) => string | undefined;
+ computeHelper?: (
+ schema: HaFormSchema,
+ localize: LocalizeFunc
+ ) => string | undefined;
+}
+
export interface LovelaceCardConstructor extends Constructor {
getStubConfig?: (
hass: HomeAssistant,
@@ -52,6 +67,7 @@ export interface LovelaceCardConstructor extends Constructor {
entitiesFallback: string[]
) => LovelaceCardConfig;
getConfigElement?: () => LovelaceCardEditor;
+ getConfigForm?: () => LovelaceConfigForm;
}
export interface LovelaceHeaderFooterConstructor
@@ -104,11 +120,15 @@ export interface LovelaceTileFeature extends HTMLElement {
export interface LovelaceTileFeatureConstructor
extends Constructor {
- getConfigElement?: () => LovelaceTileFeatureEditor;
getStubConfig?: (
hass: HomeAssistant,
stateObj?: HassEntity
) => LovelaceTileFeatureConfig;
+ getConfigElement?: () => LovelaceTileFeatureEditor;
+ getConfigForm?: () => {
+ schema: HaFormSchema[];
+ assertConfig?: (config: LovelaceCardConfig) => void;
+ };
isSupported?: (stateObj?: HassEntity) => boolean;
}
diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts
index 3bd5da6fc9..e71291bec8 100644
--- a/src/panels/media-browser/ha-panel-media-browser.ts
+++ b/src/panels/media-browser/ha-panel-media-browser.ts
@@ -9,7 +9,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
-import { LocalStorage } from "../../common/decorators/local-storage";
+import { storage } from "../../common/decorators/storage";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
import "../../components/ha-menu-button";
@@ -71,7 +71,11 @@ class PanelMediaBrowser extends LitElement {
},
];
- @LocalStorage("mediaBrowseEntityId", true, false)
+ @storage({
+ key: "mediaBrowseEntityId",
+ state: true,
+ subscribe: false,
+ })
private _entityId = BROWSER_PLAYER;
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
diff --git a/src/resources/svg-arc.ts b/src/resources/svg-arc.ts
new file mode 100644
index 0000000000..67e29b5157
--- /dev/null
+++ b/src/resources/svg-arc.ts
@@ -0,0 +1,67 @@
+type Vector = [number, number];
+type Matrix = [Vector, Vector];
+
+const rotateVector = ([[a, b], [c, d]]: Matrix, [x, y]: Vector): Vector => [
+ a * x + b * y,
+ c * x + d * y,
+];
+const createRotateMatrix = (x: number): Matrix => [
+ [Math.cos(x), -Math.sin(x)],
+ [Math.sin(x), Math.cos(x)],
+];
+const addVector = ([a1, a2]: Vector, [b1, b2]: Vector): Vector => [
+ a1 + b1,
+ a2 + b2,
+];
+
+export const toRadian = (angle: number) => (angle / 180) * Math.PI;
+
+type ArcOptions = {
+ x: number;
+ y: number;
+ r: number;
+ start: number;
+ end: number;
+ rotate?: number;
+};
+
+export const arc = (options: ArcOptions) => {
+ const { x, y, r, start, end, rotate = 0 } = options;
+ const cx = x;
+ const cy = y;
+ const rx = r;
+ const ry = r;
+ const t1 = toRadian(start);
+ const t2 = toRadian(end);
+ const delta = (t2 - t1) % (2 * Math.PI);
+ const phi = toRadian(rotate);
+
+ const rotMatrix = createRotateMatrix(phi);
+ const [sX, sY] = addVector(
+ rotateVector(rotMatrix, [rx * Math.cos(t1), ry * Math.sin(t1)]),
+ [cx, cy]
+ );
+ const [eX, eY] = addVector(
+ rotateVector(rotMatrix, [
+ rx * Math.cos(t1 + delta),
+ ry * Math.sin(t1 + delta),
+ ]),
+ [cx, cy]
+ );
+ const fA = delta > Math.PI ? 1 : 0;
+ const fS = delta > 0 ? 1 : 0;
+
+ return [
+ "M",
+ sX,
+ sY,
+ "A",
+ rx,
+ ry,
+ (phi / (2 * Math.PI)) * 360,
+ fA,
+ fS,
+ eX,
+ eY,
+ ].join(" ");
+};
diff --git a/src/state-summary/state-card-lock.js b/src/state-summary/state-card-lock.js
index d0280b7a1d..94ee6e27aa 100644
--- a/src/state-summary/state-card-lock.js
+++ b/src/state-summary/state-card-lock.js
@@ -3,8 +3,10 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
+import { supportsFeature } from "../common/entity/supports-feature";
import "../components/entity/state-info";
import LocalizeMixin from "../mixins/localize-mixin";
+import { LockEntityFeature } from "../data/lock";
/*
* @appliesMixin LocalizeMixin
@@ -19,10 +21,19 @@ class StateCardLock extends LocalizeMixin(PolymerElement) {
height: 37px;
margin-right: -0.57em;
}
+ [hidden] {
+ display: none !important;
+ }
${this.stateInfoTemplate}
+ [[localize('ui.card.lock.open')]]