diff --git a/gallery/public/images/paulus.jpg b/gallery/public/images/paulus.jpg new file mode 100644 index 000000000000..6ffa0fcdaaed Binary files /dev/null and b/gallery/public/images/paulus.jpg differ diff --git a/gallery/src/pages/lovelace/picture-card.markdown b/gallery/src/pages/lovelace/picture-card.markdown new file mode 100644 index 000000000000..4c762c101b29 --- /dev/null +++ b/gallery/src/pages/lovelace/picture-card.markdown @@ -0,0 +1,3 @@ +--- +title: Picture Card +--- diff --git a/gallery/src/pages/lovelace/picture-card.ts b/gallery/src/pages/lovelace/picture-card.ts new file mode 100644 index 000000000000..5ad709e46fe2 --- /dev/null +++ b/gallery/src/pages/lovelace/picture-card.ts @@ -0,0 +1,61 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, query } from "lit/decorators"; +import { getEntity } from "../../../../src/fake_data/entity"; +import { provideHass } from "../../../../src/fake_data/provide_hass"; +import "../../components/demo-cards"; +import { mockIcons } from "../../../../demo/src/stubs/icons"; + +const ENTITIES = [ + getEntity("person", "paulus", "home", { + friendly_name: "Paulus", + entity_picture: "/images/paulus.jpg", + }), +]; + +const CONFIGS = [ + { + heading: "Image URL", + config: ` +- type: picture + image: /images/living_room.png + `, + }, + { + heading: "Person entity", + config: ` +- type: picture + image_entity: person.paulus + `, + }, + { + heading: "Error: Image required", + config: ` +- type: picture + entity: person.paulus + `, + }, +]; + +@customElement("demo-lovelace-picture-card") +class DemoPicture extends LitElement { + @query("#demos") private _demoRoot!: HTMLElement; + + protected render(): TemplateResult { + return html``; + } + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + const hass = provideHass(this._demoRoot); + hass.updateTranslations(null, "en"); + hass.updateTranslations("lovelace", "en"); + hass.addEntities(ENTITIES); + mockIcons(hass); + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-lovelace-picture-card": DemoPicture; + } +} diff --git a/gallery/src/pages/lovelace/picture-elements-card.ts b/gallery/src/pages/lovelace/picture-elements-card.ts index 7f6b0c99cbb4..117e6c6beec6 100644 --- a/gallery/src/pages/lovelace/picture-elements-card.ts +++ b/gallery/src/pages/lovelace/picture-elements-card.ts @@ -25,6 +25,15 @@ const ENTITIES = [ friendly_name: "Movement Backyard", device_class: "motion", }), + getEntity("person", "paulus", "home", { + friendly_name: "Paulus", + entity_picture: "/images/paulus.jpg", + }), + getEntity("sensor", "battery", 35, { + device_class: "battery", + friendly_name: "Battery", + unit_of_measurement: "%", + }), ]; const CONFIGS = [ @@ -123,6 +132,19 @@ const CONFIGS = [ left: 35% `, }, + { + heading: "Person entity", + config: ` +- type: picture-elements + image_entity: person.paulus + elements: + - type: state-icon + entity: sensor.battery + style: + top: 8% + left: 8% + `, + }, ]; @customElement("demo-lovelace-picture-elements-card") diff --git a/gallery/src/pages/lovelace/picture-entity-card.ts b/gallery/src/pages/lovelace/picture-entity-card.ts index 1573f0dbec91..d97f41774659 100644 --- a/gallery/src/pages/lovelace/picture-entity-card.ts +++ b/gallery/src/pages/lovelace/picture-entity-card.ts @@ -12,6 +12,10 @@ const ENTITIES = [ getEntity("light", "bed_light", "off", { friendly_name: "Bed Light", }), + getEntity("person", "paulus", "home", { + friendly_name: "Paulus", + entity_picture: "/images/paulus.jpg", + }), ]; const CONFIGS = [ @@ -50,6 +54,13 @@ const CONFIGS = [ entity: camera.demo_camera `, }, + { + heading: "Person entity", + config: ` +- type: picture-entity + entity: person.paulus + `, + }, { heading: "Hidden name", config: ` diff --git a/gallery/src/pages/lovelace/picture-glance-card.ts b/gallery/src/pages/lovelace/picture-glance-card.ts index dccc05e09b52..91f2e4dca5ac 100644 --- a/gallery/src/pages/lovelace/picture-glance-card.ts +++ b/gallery/src/pages/lovelace/picture-glance-card.ts @@ -20,6 +20,15 @@ const ENTITIES = [ friendly_name: "Basement Floor Wet", device_class: "moisture", }), + getEntity("person", "paulus", "home", { + friendly_name: "Paulus", + entity_picture: "/images/paulus.jpg", + }), + getEntity("sensor", "battery", 35, { + device_class: "battery", + friendly_name: "Battery", + unit_of_measurement: "%", + }), ]; const CONFIGS = [ @@ -90,6 +99,15 @@ const CONFIGS = [ - light.ceiling_lights `, }, + { + heading: "Person entity", + config: ` +- type: picture-glance + image_entity: person.paulus + entities: + - sensor.battery + `, + }, { heading: "Custom icon", config: ` diff --git a/src/data/person.ts b/src/data/person.ts index d1a0cbdd6a45..e8ee0ba25c2d 100644 --- a/src/data/person.ts +++ b/src/data/person.ts @@ -1,3 +1,7 @@ +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; export interface BasePerson { @@ -18,6 +22,20 @@ export interface PersonMutableParams { picture: string | null; } +interface PersonEntityAttributes extends HassEntityAttributeBase { + id?: string; + user_id?: string; + device_trackers?: string[]; + editable?: boolean; + gps_accuracy?: number; + latitude?: number; + longitude?: number; +} + +export interface PersonEntity extends HassEntityBase { + attributes: PersonEntityAttributes; +} + export const fetchPersons = (hass: HomeAssistant) => hass.callWS<{ storage: Person[]; diff --git a/src/panels/lovelace/cards/hui-picture-card.ts b/src/panels/lovelace/cards/hui-picture-card.ts index be5902ba84ae..f5abf64b5897 100644 --- a/src/panels/lovelace/cards/hui-picture-card.ts +++ b/src/panels/lovelace/cards/hui-picture-card.ts @@ -10,6 +10,7 @@ import { customElement, property, state } 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 { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-card"; import { computeImageUrl, ImageEntity } from "../../../data/image"; import { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; @@ -21,6 +22,7 @@ import { hasConfigChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { PictureCardConfig } from "./types"; +import { PersonEntity } from "../../../data/person"; @customElement("hui-picture-card") export class HuiPictureCard extends LitElement implements LovelaceCard { @@ -95,10 +97,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard { return nothing; } - let stateObj: ImageEntity | undefined; + let stateObj: ImageEntity | PersonEntity | undefined; if (this._config.image_entity) { - stateObj = this.hass.states[this._config.image_entity] as ImageEntity; + stateObj = this.hass.states[this._config.image_entity]; if (!stateObj) { return html` ${createEntityNotFoundWarning(this.hass, this._config.image_entity)} @@ -106,6 +108,21 @@ export class HuiPictureCard extends LitElement implements LovelaceCard { } } + let image: string | undefined = this._config.image; + if (this._config.image_entity) { + const domain: string = computeDomain(this._config.image_entity); + switch (domain) { + case "image": + image = computeImageUrl(stateObj as ImageEntity); + break; + case "person": + if ((stateObj as PersonEntity).attributes.entity_picture) { + image = (stateObj as PersonEntity).attributes.entity_picture; + } + break; + } + } + 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 09f0455c2adb..143a2fde5858 100644 --- a/src/panels/lovelace/cards/hui-picture-elements-card.ts +++ b/src/panels/lovelace/cards/hui-picture-elements-card.ts @@ -8,6 +8,7 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; +import { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-card"; import { ImageEntity, computeImageUrl } from "../../../data/image"; import { HomeAssistant } from "../../../types"; @@ -16,6 +17,7 @@ import { LovelaceElement, LovelaceElementConfig } from "../elements/types"; import { LovelaceCard } from "../types"; import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element"; import { PictureElementsCardConfig } from "./types"; +import { PersonEntity } from "../../../data/person"; @customElement("hui-picture-elements-card") class HuiPictureElementsCard extends LitElement implements LovelaceCard { @@ -116,9 +118,21 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard { return nothing; } - let stateObj: ImageEntity | undefined; + let image: string | undefined = this._config.image; if (this._config.image_entity) { - stateObj = this.hass.states[this._config.image_entity] as ImageEntity; + const stateObj: ImageEntity | PersonEntity | undefined = + this.hass.states[this._config.image_entity]; + const domain: string = computeDomain(this._config.image_entity); + switch (domain) { + case "image": + image = computeImageUrl(stateObj as ImageEntity); + break; + case "person": + if ((stateObj as PersonEntity).attributes.entity_picture) { + image = (stateObj as PersonEntity).attributes.entity_picture; + } + break; + } } return html` @@ -126,7 +140,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
${entityState}
`; } - const domain = computeDomain(this._config.entity); + const domain: string = computeDomain(this._config.entity); + let image: string | undefined = this._config.image; + switch (domain) { + case "image": + image = computeImageUrl(stateObj as ImageEntity); + break; + case "person": + if ((stateObj as PersonEntity).attributes.entity_picture) { + image = (stateObj as PersonEntity).attributes.entity_picture; + } + break; + } return html` ; 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 f065215ce959..c2e8920fdeb2 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -115,7 +115,7 @@ export const computeSection = ( type: "tile", entity, show_entity_picture: - ["person", "camera", "image"].includes(computeDomain(entity)) || + ["camera", "image", "person"].includes(computeDomain(entity)) || undefined, }) as TileCardConfig ), 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 20ff12e88b41..6bd9d93cba2a 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 @@ -25,7 +25,10 @@ const cardConfigStruct = assign( const SCHEMA = [ { name: "image", selector: { image: {} } }, - { name: "image_entity", selector: { entity: { domain: "image" } } }, + { + name: "image_entity", + selector: { entity: { domain: ["image", "person"] } }, + }, { name: "alt_text", selector: { text: {} } }, { name: "theme", selector: { theme: {} } }, { 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 1c74428d3848..8ed4b388db9f 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 @@ -36,7 +36,10 @@ const cardConfigStruct = assign( const SCHEMA = [ { name: "title", selector: { text: {} } }, { name: "image", selector: { image: {} } }, - { name: "image_entity", selector: { entity: { domain: "image" } } }, + { + name: "image_entity", + selector: { entity: { domain: ["image", "person"] } }, + }, { name: "camera_image", selector: { entity: { domain: "camera" } } }, { name: "",