diff --git a/src/components/entity/ha-entity-state-picker.ts b/src/components/entity/ha-entity-state-picker.ts index 9cd3b05f7a..0ecf1e9703 100644 --- a/src/components/entity/ha-entity-state-picker.ts +++ b/src/components/entity/ha-entity-state-picker.ts @@ -63,10 +63,10 @@ class HaEntityStatePicker extends LitElement { const entityIds = this.entityId ? ensureArray(this.entityId) : []; const entitiesOptions = entityIds.map((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) diff --git a/src/data/person.ts b/src/data/person.ts index 62145b875d..0bd692b876 100644 --- a/src/data/person.ts +++ b/src/data/person.ts @@ -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 = {}; + +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; +}; diff --git a/src/panels/lovelace/common/icon-condition.ts b/src/panels/lovelace/common/icon-condition.ts index 02aa15ac25..9bb50b1b64 100644 --- a/src/panels/lovelace/common/icon-condition.ts +++ b/src/panels/lovelace/common/icon-condition.ts @@ -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 = { + location: mdiMapMarker, numeric_state: mdiNumeric, state: mdiStateMachine, screen: mdiResponsive, diff --git a/src/panels/lovelace/common/validate-condition.ts b/src/panels/lovelace/common/validate-condition.ts index 3c7ea7cfe9..332b75c224 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -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": diff --git a/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts b/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts index e1dfaa6fb7..ef09fbd957 100644 --- a/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts +++ b/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts @@ -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", diff --git a/src/panels/lovelace/editor/conditions/types/ha-card-condition-location.ts b/src/panels/lovelace/editor/conditions/types/ha-card-condition-location.ts new file mode 100644 index 0000000000..6cd0ecfdc5 --- /dev/null +++ b/src/panels/lovelace/editor/conditions/types/ha-card-condition-location.ts @@ -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` + + `; + } + + 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; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index e831f61d7d..f3c0c6a626 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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" },