mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add support for image entity (#16877)
This commit is contained in:
parent
1fe5d66a68
commit
eb552530e2
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
54
src/data/image_upload.ts
Normal 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,
|
||||
});
|
@ -40,6 +40,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"fan",
|
||||
"group",
|
||||
"humidifier",
|
||||
"image",
|
||||
"input_boolean",
|
||||
"input_datetime",
|
||||
"light",
|
||||
|
40
src/dialogs/more-info/controls/more-info-image.ts
Normal file
40
src/dialogs/more-info/controls/more-info-image.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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"),
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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) =>
|
||||
|
@ -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>;
|
||||
|
@ -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",
|
||||
|
@ -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, {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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: "",
|
||||
|
@ -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}
|
||||
|
@ -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"];
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user