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