Add support for image entity (#16877)

This commit is contained in:
Bram Kragten 2023-06-21 17:46:40 +02:00 committed by GitHub
parent 1fe5d66a68
commit eb552530e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 287 additions and 202 deletions

View File

@ -33,6 +33,7 @@ import {
mdiGoogleCirclesCommunities,
mdiHomeAssistant,
mdiHomeAutomation,
mdiImage,
mdiImageFilterFrames,
mdiLightbulb,
mdiLightningBolt,
@ -90,6 +91,7 @@ export const FIXED_DOMAIN_ICONS = {
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,
homekit: mdiHomeAutomation,
image: mdiImage,
image_processing: mdiImageFilterFrames,
input_button: mdiGestureTapButton,
input_datetime: mdiCalendarClock,

View File

@ -191,7 +191,9 @@ export const computeStateDisplayFromEntityAttributes = (
// state is a timestamp
if (
["button", "input_button", "scene", "stt", "tts"].includes(domain) ||
["button", "image", "input_button", "scene", "stt", "tts"].includes(
domain
) ||
(domain === "sensor" && attributes.device_class === "timestamp")
) {
try {

View File

@ -2,7 +2,7 @@ import { mdiImagePlus } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { createImage, generateImageThumbnailUrl } from "../data/image";
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import {
CropOptions,

View File

@ -1,54 +1,15 @@
import { HomeAssistant } from "../types";
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
interface Image {
filesize: number;
name: string;
uploaded_at: string; // isoformat date
content_type: string;
id: string;
interface ImageEntityAttributes extends HassEntityAttributeBase {
access_token: string;
}
export interface ImageMutableParams {
name: string;
export interface ImageEntity extends HassEntityBase {
attributes: ImageEntityAttributes;
}
export const generateImageThumbnailUrl = (mediaId: string, size: number) =>
`/api/image/serve/${mediaId}/${size}x${size}`;
export const fetchImages = (hass: HomeAssistant) =>
hass.callWS<Image[]>({ type: "image/list" });
export const createImage = async (
hass: HomeAssistant,
file: File
): Promise<Image> => {
const fd = new FormData();
fd.append("file", file);
const resp = await hass.fetchWithAuth("/api/image/upload", {
method: "POST",
body: fd,
});
if (resp.status === 413) {
throw new Error(`Uploaded image is too large (${file.name})`);
} else if (resp.status !== 200) {
throw new Error("Unknown error");
}
return resp.json();
};
export const updateImage = (
hass: HomeAssistant,
id: string,
updates: Partial<ImageMutableParams>
) =>
hass.callWS<Image>({
type: "image/update",
media_id: id,
...updates,
});
export const deleteImage = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "image/delete",
media_id: id,
});
export const computeImageUrl = (entity: ImageEntity): string =>
`/api/image_proxy/${entity.entity_id}?token=${entity.attributes.access_token}&state=${entity.state}`;

54
src/data/image_upload.ts Normal file
View File

@ -0,0 +1,54 @@
import { HomeAssistant } from "../types";
interface Image {
filesize: number;
name: string;
uploaded_at: string; // isoformat date
content_type: string;
id: string;
}
export interface ImageMutableParams {
name: string;
}
export const generateImageThumbnailUrl = (mediaId: string, size: number) =>
`/api/image/serve/${mediaId}/${size}x${size}`;
export const fetchImages = (hass: HomeAssistant) =>
hass.callWS<Image[]>({ type: "image/list" });
export const createImage = async (
hass: HomeAssistant,
file: File
): Promise<Image> => {
const fd = new FormData();
fd.append("file", file);
const resp = await hass.fetchWithAuth("/api/image/upload", {
method: "POST",
body: fd,
});
if (resp.status === 413) {
throw new Error(`Uploaded image is too large (${file.name})`);
} else if (resp.status !== 200) {
throw new Error("Unknown error");
}
return resp.json();
};
export const updateImage = (
hass: HomeAssistant,
id: string,
updates: Partial<ImageMutableParams>
) =>
hass.callWS<Image>({
type: "image/update",
media_id: id,
...updates,
});
export const deleteImage = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "image/delete",
media_id: id,
});

View File

@ -40,6 +40,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"fan",
"group",
"humidifier",
"image",
"input_boolean",
"input_datetime",
"light",

View File

@ -0,0 +1,40 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-camera-stream";
import { computeImageUrl, ImageEntity } from "../../../data/image";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-image")
class MoreInfoImage extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: ImageEntity;
protected render() {
if (!this.hass || !this.stateObj) {
return nothing;
}
return html`<img
alt=${this.stateObj.attributes.friendly_name || this.stateObj.entity_id}
src=${this.hass.hassUrl(computeImageUrl(this.stateObj))}
/> `;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
text-align: center;
}
img {
max-width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-image": MoreInfoImage;
}
}

View File

@ -18,6 +18,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
fan: () => import("./controls/more-info-fan"),
group: () => import("./controls/more-info-group"),
humidifier: () => import("./controls/more-info-humidifier"),
image: () => import("./controls/more-info-image"),
input_boolean: () => import("./controls/more-info-input_boolean"),
input_datetime: () => import("./controls/more-info-input_datetime"),
light: () => import("./controls/more-info-light"),

View File

@ -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`<hui-warning>
${createEntityNotFoundWarning(this.hass, this._config.image_entity)}
</hui-warning>`;
}
}
return html`
<ha-card
@action=${this._handleAction}
@ -91,19 +114,29 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
tabindex=${ifDefined(
hasAction(this._config.tap_action) ? "0" : undefined
hasAction(this._config.tap_action) || this._config.image_entity
? "0"
: undefined
)}
class=${classMap({
clickable: Boolean(
this._config.tap_action ||
this._config.hold_action ||
this._config.double_tap_action
(this._config.image_entity && !this._config.tap_action) ||
(this._config.tap_action &&
this._config.tap_action.action !== "none") ||
(this._config.hold_action &&
this._config.hold_action.action !== "none") ||
(this._config.double_tap_action &&
this._config.double_tap_action.action !== "none")
),
})}
>
<img
alt=${this._config.alt_text}
src=${this.hass.hassUrl(this._config.image)}
alt=${ifDefined(
this._config.alt_text || stateObj?.attributes.friendly_name
)}
src=${this.hass.hassUrl(
stateObj ? computeImageUrl(stateObj) : this._config.image
)}
/>
</ha-card>
`;

View File

@ -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`
<ha-card .header=${this._config.title}>
<div id="root">
<hui-image
.hass=${this.hass}
.image=${this._config.image}
.image=${stateObj ? computeImageUrl(stateObj) : this._config.image}
.stateImage=${this._config.state_image}
.stateFilter=${this._config.state_filter}
.cameraImage=${this._config.camera_image}

View File

@ -14,6 +14,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name";
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";
@ -68,7 +69,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
}
if (
computeDomain(config.entity) !== "camera" &&
!["camera", "image"].includes(computeDomain(config.entity)) &&
!config.image &&
!config.state_image &&
!config.camera_image
@ -141,14 +142,18 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
footer = html`<div class="footer single">${entityState}</div>`;
}
const domain = computeDomain(this._config.entity);
return html`
<ha-card>
<hui-image
.hass=${this.hass}
.image=${this._config.image}
.image=${domain === "image"
? computeImageUrl(stateObj as ImageEntity)
: this._config.image}
.stateImage=${this._config.state_image}
.stateFilter=${this._config.state_filter}
.cameraImage=${computeDomain(this._config.entity) === "camera"
.cameraImage=${domain === "camera"
? this._config.entity
: this._config.camera_image}
.cameraView=${this._config.camera_view}

View File

@ -3,9 +3,9 @@ import {
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@ -18,6 +18,7 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-state-icon";
import { computeImageUrl, ImageEntity } from "../../../data/image";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
@ -63,7 +64,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
};
}
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: PictureGlanceCardConfig;
@ -80,7 +81,12 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
!config ||
!config.entities ||
!Array.isArray(config.entities) ||
!(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("Invalid configuration");
@ -108,25 +114,35 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (hasConfigOrEntityChanged(this, changedProps)) {
if (!this._config || hasConfigOrEntityChanged(this, changedProps)) {
return true;
}
if (!changedProps.has("hass")) {
return false;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
!oldHass ||
oldHass.themes !== this.hass!.themes ||
oldHass.locale !== this.hass!.locale
oldHass.themes !== this.hass.themes ||
oldHass.locale !== this.hass.locale
) {
return true;
}
if (
this._config.image_entity &&
oldHass.states[this._config.image_entity] !==
this.hass.states[this._config.image_entity]
) {
return true;
}
if (this._entitiesDialog) {
for (const entity of this._entitiesDialog) {
if (
oldHass!.states[entity.entity] !== this.hass!.states[entity.entity]
) {
if (oldHass.states[entity.entity] !== this.hass.states[entity.entity]) {
return true;
}
}
@ -134,9 +150,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
if (this._entitiesToggle) {
for (const entity of this._entitiesToggle) {
if (
oldHass!.states[entity.entity] !== this.hass!.states[entity.entity]
) {
if (oldHass.states[entity.entity] !== this.hass.states[entity.entity]) {
return true;
}
}
@ -170,6 +184,11 @@ class HuiPictureGlanceCard 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`
<ha-card>
<hui-image
@ -177,7 +196,8 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
clickable: Boolean(
this._config.tap_action ||
this._config.hold_action ||
this._config.camera_image
this._config.camera_image ||
this._config.image_entity
),
})}
@action=${this._handleAction}
@ -190,7 +210,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
)}
.config=${this._config}
.hass=${this.hass}
.image=${this._config.image}
.image=${stateObj ? computeImageUrl(stateObj) : this._config.image}
.stateImage=${this._config.state_image}
.stateFilter=${this._config.state_filter}
.cameraImage=${this._config.camera_image}
@ -200,7 +220,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
></hui-image>
<div class="box">
${this._config.title
? html` <div class="title">${this._config.title}</div> `
? html`<div class="title">${this._config.title}</div>`
: ""}
<div class="row">
${this._entitiesDialog!.map((entityConf) =>

View File

@ -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<string, unknown>;

View File

@ -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",

View File

@ -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, {

View File

@ -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;
}

View File

@ -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`
<div class="card-config">
<ha-textfield
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.image"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
.value=${this._image}
.configValue=${"image"}
@input=${this._valueChanged}
></ha-textfield>
<ha-textfield
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.alt_text"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._alt_text}
.configValue=${"alt_text"}
@input=${this._valueChanged}
></ha-textfield>
<ha-theme-picker
.hass=${this.hass}
.value=${this._theme}
.label=${`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></ha-theme-picker>
<hui-action-editor
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.tap_action"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.hass=${this.hass}
.config=${this._tap_action}
.actions=${actions}
.configValue=${"tap_action"}
@value-changed=${this._valueChanged}
></hui-action-editor>
<hui-action-editor
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.hold_action"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.hass=${this.hass}
.config=${this._hold_action}
.actions=${actions}
.configValue=${"hold_action"}
@value-changed=${this._valueChanged}
></hui-action-editor>
</div>
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
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<typeof SCHEMA>) => {
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 {

View File

@ -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: "",

View File

@ -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`
<hui-image
.hass=${this.hass}
.entity=${this._config.entity}
.image=${this._config.image}
.image=${stateObj ? computeImageUrl(stateObj) : this._config.image}
.stateImage=${this._config.state_image}
.cameraImage=${this._config.camera_image}
.cameraView=${this._config.camera_view}

View File

@ -42,6 +42,7 @@ export interface ImageElementConfig extends LovelaceElementConfigBase {
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
image?: string;
image_entity?: string;
state_image?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];

View File

@ -4667,6 +4667,7 @@
"aspect_ratio": "Aspect Ratio",
"attribute": "Attribute",
"camera_image": "Camera Entity",
"image_entity": "Image Entity",
"camera_view": "Camera View",
"double_tap_action": "Double Tap Action",
"entities": "Entities",