mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 11:46:42 +00:00
picture cards: add person image support (#20593)
* picture cards: add person image support * fix: person attributes typing * review: apply comment from @coderabbitai * fix lint:types * review: put person domain in image_entity config * add picture card compatibility & exemple in gallery * fix lint * Allow only image or person domains on image_entity editor config Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> * fix domain type * gracefully use the default config.image if the person don't have an image * gracefully use the default config.image if the person don't have an image (that works) --------- Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
This commit is contained in:
parent
567a2ea019
commit
87ba0e73dd
BIN
gallery/public/images/paulus.jpg
Normal file
BIN
gallery/public/images/paulus.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
3
gallery/src/pages/lovelace/picture-card.markdown
Normal file
3
gallery/src/pages/lovelace/picture-card.markdown
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Picture Card
|
||||
---
|
61
gallery/src/pages/lovelace/picture-card.ts
Normal file
61
gallery/src/pages/lovelace/picture-card.ts
Normal file
@ -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`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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: `
|
||||
|
@ -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: `
|
||||
|
@ -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[];
|
||||
|
@ -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`<hui-warning>
|
||||
${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`
|
||||
<ha-card
|
||||
@action=${this._handleAction}
|
||||
@ -134,9 +151,7 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||
alt=${ifDefined(
|
||||
this._config.alt_text || stateObj?.attributes.friendly_name
|
||||
)}
|
||||
src=${this.hass.hassUrl(
|
||||
stateObj ? computeImageUrl(stateObj) : this._config.image
|
||||
)}
|
||||
src=${this.hass.hassUrl(image)}
|
||||
/>
|
||||
</ha-card>
|
||||
`;
|
||||
|
@ -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 {
|
||||
<div id="root">
|
||||
<hui-image
|
||||
.hass=${this.hass}
|
||||
.image=${stateObj ? computeImageUrl(stateObj) : this._config.image}
|
||||
.image=${image}
|
||||
.stateImage=${this._config.state_image}
|
||||
.stateFilter=${this._config.state_filter}
|
||||
.cameraImage=${this._config.camera_image}
|
||||
|
@ -25,6 +25,8 @@ import "../components/hui-image";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { PictureEntityCardConfig } from "./types";
|
||||
import { CameraEntity } from "../../../data/camera";
|
||||
import { PersonEntity } from "../../../data/person";
|
||||
|
||||
@customElement("hui-picture-entity-card")
|
||||
class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
||||
@ -68,7 +70,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
if (
|
||||
!["camera", "image"].includes(computeDomain(config.entity)) &&
|
||||
!["camera", "image", "person"].includes(computeDomain(config.entity)) &&
|
||||
!config.image &&
|
||||
!config.state_image &&
|
||||
!config.camera_image
|
||||
@ -108,7 +110,8 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
const stateObj: CameraEntity | ImageEntity | PersonEntity | undefined =
|
||||
this.hass.states[this._config.entity];
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
@ -135,15 +138,24 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
||||
footer = html`<div class="footer single">${entityState}</div>`;
|
||||
}
|
||||
|
||||
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`
|
||||
<ha-card>
|
||||
<hui-image
|
||||
.hass=${this.hass}
|
||||
.image=${domain === "image"
|
||||
? computeImageUrl(stateObj as ImageEntity)
|
||||
: this._config.image}
|
||||
.image=${image}
|
||||
.stateImage=${this._config.state_image}
|
||||
.stateFilter=${this._config.state_filter}
|
||||
.cameraImage=${domain === "camera"
|
||||
|
@ -31,6 +31,7 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import "../components/hui-warning-element";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { PictureGlanceCardConfig, PictureGlanceEntityConfig } from "./types";
|
||||
import { PersonEntity } from "../../../data/person";
|
||||
|
||||
const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]);
|
||||
|
||||
@ -183,9 +184,21 @@ class HuiPictureGlanceCard 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`
|
||||
@ -209,7 +222,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
||||
)}
|
||||
.config=${this._config}
|
||||
.hass=${this.hass}
|
||||
.image=${stateObj ? computeImageUrl(stateObj) : this._config.image}
|
||||
.image=${image}
|
||||
.stateImage=${this._config.state_image}
|
||||
.stateFilter=${this._config.state_filter}
|
||||
.cameraImage=${this._config.camera_image}
|
||||
|
@ -406,6 +406,7 @@ export interface PictureGlanceCardConfig extends LovelaceCardConfig {
|
||||
entities: Array<string | PictureGlanceEntityConfig>;
|
||||
title?: string;
|
||||
image?: string;
|
||||
image_entity?: string;
|
||||
camera_image?: string;
|
||||
camera_view?: HuiImage["cameraView"];
|
||||
state_image?: Record<string, unknown>;
|
||||
|
@ -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
|
||||
),
|
||||
|
@ -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: {} } },
|
||||
{
|
||||
|
@ -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: "",
|
||||
|
Loading…
x
Reference in New Issue
Block a user