Add a dashboard condition based on user's location (#26401)

* Add a dashboard condition based on user's location

* Update src/translations/en.json

Co-authored-by: Norbert Rittel <norbert@rittel.de>

* Update src/data/person.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Use multiple: true

---------

Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
karwosts
2025-08-11 21:51:31 -07:00
committed by GitHub
parent fe762e9ae4
commit 055e65c45e
7 changed files with 173 additions and 4 deletions

View File

@@ -63,10 +63,10 @@ class HaEntityStatePicker extends LitElement {
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
const stateObj = this.hass.states[entityId];
if (!stateObj) {
return [];
}
const stateObj = this.hass.states[entityId] || {
entity_id: entityId,
attributes: {},
};
const states = getStates(this.hass, stateObj, this.attribute).filter(
(s) => !this.hideStates?.includes(s)

View File

@@ -1,7 +1,10 @@
import type {
HassEntity,
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import type { HomeAssistant } from "../types";
export interface BasePerson {
@@ -67,3 +70,28 @@ export const deletePerson = (hass: HomeAssistant, personId: string) =>
type: "person/delete",
person_id: personId,
});
const cachedUserPerson: Record<string, string> = {};
export const getUserPerson = (hass: HomeAssistant): undefined | HassEntity => {
if (!hass.user?.id) {
return undefined;
}
const cachedPersonEntityId = cachedUserPerson[hass.user.id];
if (cachedPersonEntityId) {
const stateObj = hass.states[cachedPersonEntityId];
if (stateObj && stateObj.attributes.user_id === hass.user.id) {
return stateObj;
}
}
const result = Object.values(hass.states).find(
(state) =>
state.attributes.user_id === hass.user!.id &&
computeStateDomain(state) === "person"
);
if (result) {
cachedUserPerson[hass.user.id] = result.entity_id;
}
return result;
};

View File

@@ -2,6 +2,7 @@ import {
mdiAccount,
mdiAmpersand,
mdiGateOr,
mdiMapMarker,
mdiNotEqualVariant,
mdiNumeric,
mdiResponsive,
@@ -10,6 +11,7 @@ import {
import type { Condition } from "./validate-condition";
export const ICON_CONDITION: Record<Condition["condition"], string> = {
location: mdiMapMarker,
numeric_state: mdiNumeric,
state: mdiStateMachine,
screen: mdiResponsive,

View File

@@ -1,11 +1,14 @@
import { ensureArray } from "../../../common/array/ensure-array";
import type { MediaQueriesListener } from "../../../common/dom/media_query";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { UNKNOWN } from "../../../data/entity";
import { getUserPerson } from "../../../data/person";
import type { HomeAssistant } from "../../../types";
export type Condition =
| LocationCondition
| NumericStateCondition
| StateCondition
| ScreenCondition
@@ -25,6 +28,11 @@ interface BaseCondition {
condition: string;
}
export interface LocationCondition extends BaseCondition {
condition: "location";
locations?: string[];
}
export interface NumericStateCondition extends BaseCondition {
condition: "numeric_state";
entity?: string;
@@ -144,6 +152,17 @@ function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) {
: false;
}
function checkLocationCondition(
condition: LocationCondition,
hass: HomeAssistant
) {
const stateObj = getUserPerson(hass);
if (!stateObj) {
return false;
}
return condition.locations?.includes(stateObj.state);
}
function checkUserCondition(condition: UserCondition, hass: HomeAssistant) {
return condition.users && hass.user?.id
? condition.users.includes(hass.user.id)
@@ -182,6 +201,8 @@ export function checkConditionsMet(
return checkScreenCondition(c, hass);
case "user":
return checkUserCondition(c, hass);
case "location":
return checkLocationCondition(c, hass);
case "numeric_state":
return checkStateNumericCondition(c, hass);
case "and":
@@ -256,6 +277,10 @@ function validateUserCondition(condition: UserCondition) {
return condition.users != null;
}
function validateLocationCondition(condition: LocationCondition) {
return condition.locations != null;
}
function validateAndCondition(condition: AndCondition) {
return condition.conditions != null;
}
@@ -289,6 +314,8 @@ export function validateConditionalConfig(
return validateScreenCondition(c);
case "user":
return validateUserCondition(c);
case "location":
return validateLocationCondition(c);
case "numeric_state":
return validateNumericStateCondition(c);
case "and":

View File

@@ -18,6 +18,7 @@ import "./ha-card-condition-editor";
import type { HaCardConditionEditor } from "./ha-card-condition-editor";
import type { LovelaceConditionEditorConstructor } from "./types";
import "./types/ha-card-condition-and";
import "./types/ha-card-condition-location";
import "./types/ha-card-condition-not";
import "./types/ha-card-condition-numeric_state";
import "./types/ha-card-condition-or";
@@ -26,6 +27,7 @@ import "./types/ha-card-condition-state";
import "./types/ha-card-condition-user";
const UI_CONDITION = [
"location",
"numeric_state",
"state",
"screen",

View File

@@ -0,0 +1,105 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { array, assert, literal, object, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-check-list-item";
import "../../../../../components/ha-switch";
import "../../../../../components/ha-list";
import type { HomeAssistant } from "../../../../../types";
import type { LocationCondition } from "../../../common/validate-condition";
import "../../../../../components/ha-form/ha-form";
const locationConditionStruct = object({
condition: literal("location"),
locations: array(string()),
});
const SCHEMA = [
{
name: "locations",
selector: {
state: {
entity_id: "person.whomever",
hide_states: ["unavailable", "unknown"],
multiple: true,
},
},
},
];
@customElement("ha-card-condition-location")
export class HaCardConditionLocation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: LocationCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): LocationCondition {
return { condition: "location", locations: [] };
}
protected static validateUIConfig(condition: LocationCondition) {
return assert(condition, locationConditionStruct);
}
protected render() {
return html`
<ha-form
.hass=${this.hass}
.data=${this.condition}
.schema=${SCHEMA}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
></ha-form>
`;
}
private _valueChanged(ev) {
ev.stopPropagation();
const locations = ev.detail.value.locations;
const condition: LocationCondition = {
...this.condition,
locations,
};
fireEvent(this, "value-changed", { value: condition });
}
private _computeLabelCallback = (schema): string => {
switch (schema.name) {
case "locations":
return this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.condition.location.locations"
);
default:
return "";
}
};
private _computeHelperCallback = (schema): string => {
switch (schema.name) {
case "locations":
return this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.condition.location.locations_helper"
);
default:
return "";
}
};
static styles = css`
:host {
display: block;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-location": HaCardConditionLocation;
}
}

View File

@@ -7301,6 +7301,11 @@
"state_equal": "State is equal to",
"state_not_equal": "State is not equal to"
},
"location": {
"label": "Location",
"locations": "Locations",
"locations_helper": "This condition will be true if the person entity of the user viewing this dashboard has a state matching any of the selected zones."
},
"user": {
"label": "User"
},