Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

picture cards: add person image support #20593

Merged
merged 11 commits into from
Jul 23, 2024
Binary file added gallery/public/images/paulus.jpg
Quentame marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions gallery/src/pages/lovelace/picture-card.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
title: Picture Card
---
61 changes: 61 additions & 0 deletions gallery/src/pages/lovelace/picture-card.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
22 changes: 22 additions & 0 deletions gallery/src/pages/lovelace/picture-elements-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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")
Expand Down
11 changes: 11 additions & 0 deletions gallery/src/pages/lovelace/picture-entity-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -50,6 +54,13 @@ const CONFIGS = [
entity: camera.demo_camera
`,
},
{
heading: "Person entity",
config: `
- type: picture-entity
entity: person.paulus
`,
},
{
heading: "Hidden name",
config: `
Expand Down
18 changes: 18 additions & 0 deletions gallery/src/pages/lovelace/picture-glance-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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: `
Expand Down
18 changes: 18 additions & 0 deletions src/data/person.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";

export interface BasePerson {
Expand All @@ -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;
}

Copy link
Contributor

@coderabbitai coderabbitai bot Jun 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the attributes field from fetchPersons is properly handled in all calling contexts.

Would you like me to help implement or verify this handling?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Quentame, it appears that the attributes field from the PersonEntity is not directly referenced or manipulated in the ha-config-person.ts file or other related components in the src/panels/config/person/ directory. This suggests that the handling of the attributes field might be abstracted elsewhere or not required in the current context of the fetchPersons usage.

If there are specific functionalities or components where you expect the attributes field to be used, please let me know, and I can further investigate those areas. Otherwise, the current implementation in ha-config-person.ts seems to focus primarily on sorting and managing person data without directly interacting with the detailed attributes of PersonEntity.

Analysis chain
Scripts executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Find all files that call `fetchPersons` and display the relevant code snippets.
rg "fetchPersons" --context 5

Length of output: 2122



Script:

#!/bin/bash
# Search for direct references to the `attributes` field in `ha-config-person.ts` and other related components.
rg "attributes" --files-with-matches src/panels/config/person/

Length of output: 62

export const fetchPersons = (hass: HomeAssistant) =>
hass.callWS<{
storage: Person[];
Expand Down
25 changes: 20 additions & 5 deletions src/panels/lovelace/cards/hui-picture-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -95,17 +97,32 @@ 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)}
</hui-warning>`;
}
}

let image: string | undefined = this._config.image;
if (this._config.image_entity) {
const domain: string | undefined = computeDomain(
this._config.image_entity
);
switch (domain) {
case "image":
image = computeImageUrl(stateObj as ImageEntity);
break;
case "person":
image = (stateObj as PersonEntity).attributes.entity_picture;
break;
}
}

return html`
<ha-card
@action=${this._handleAction}
Expand Down Expand Up @@ -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>
`;
Expand Down
20 changes: 17 additions & 3 deletions src/panels/lovelace/cards/hui-picture-elements-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -116,17 +118,29 @@ 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 | undefined = computeDomain(
this._config.image_entity
);
switch (domain) {
case "image":
image = computeImageUrl(stateObj as ImageEntity);
break;
case "person":
image = (stateObj as PersonEntity).attributes.entity_picture;
break;
}
}

return html`
<ha-card .header=${this._config.title}>
<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}
Expand Down
20 changes: 15 additions & 5 deletions src/panels/lovelace/cards/hui-picture-entity-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -136,14 +139,21 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
}

const domain = computeDomain(this._config.entity);
let image: string | undefined = this._config.image;
switch (domain) {
case "image":
image = computeImageUrl(stateObj as ImageEntity);
break;
case "person":
image = (stateObj as PersonEntity).attributes.entity_picture;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently, picture entity cards are already used for person entities. The default generated lovelace config adds picture entity cards for every known person. If that person entity doesn't have a picture, entity_picture is undefined and it should fallback to the this._config.image.

Suggested change
image = (stateObj as PersonEntity).attributes.entity_picture;
if ((stateObj as PersonEntity).attributes.entity_picture) {
image = (stateObj as PersonEntity).attributes.entity_picture;
}

Copy link
Member Author

@Quentame Quentame Jul 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 349b116 0eb8b77 (Ups 😄)

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"
Expand Down
19 changes: 16 additions & 3 deletions src/panels/lovelace/cards/hui-picture-glance-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);

Expand Down Expand Up @@ -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 | undefined = computeDomain(
this._config.image_entity
);
switch (domain) {
case "image":
image = computeImageUrl(stateObj as ImageEntity);
break;
case "person":
image = (stateObj as PersonEntity).attributes.entity_picture;
break;
}
}

return html`
Expand All @@ -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}
Expand Down
Loading
Loading