mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 10:59:50 +00:00
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:
@@ -63,10 +63,10 @@ class HaEntityStatePicker extends LitElement {
|
|||||||
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
||||||
|
|
||||||
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
|
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
|
||||||
const stateObj = this.hass.states[entityId];
|
const stateObj = this.hass.states[entityId] || {
|
||||||
if (!stateObj) {
|
entity_id: entityId,
|
||||||
return [];
|
attributes: {},
|
||||||
}
|
};
|
||||||
|
|
||||||
const states = getStates(this.hass, stateObj, this.attribute).filter(
|
const states = getStates(this.hass, stateObj, this.attribute).filter(
|
||||||
(s) => !this.hideStates?.includes(s)
|
(s) => !this.hideStates?.includes(s)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import type {
|
import type {
|
||||||
|
HassEntity,
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
HassEntityBase,
|
HassEntityBase,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
export interface BasePerson {
|
export interface BasePerson {
|
||||||
@@ -67,3 +70,28 @@ export const deletePerson = (hass: HomeAssistant, personId: string) =>
|
|||||||
type: "person/delete",
|
type: "person/delete",
|
||||||
person_id: personId,
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
mdiAccount,
|
mdiAccount,
|
||||||
mdiAmpersand,
|
mdiAmpersand,
|
||||||
mdiGateOr,
|
mdiGateOr,
|
||||||
|
mdiMapMarker,
|
||||||
mdiNotEqualVariant,
|
mdiNotEqualVariant,
|
||||||
mdiNumeric,
|
mdiNumeric,
|
||||||
mdiResponsive,
|
mdiResponsive,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
import type { Condition } from "./validate-condition";
|
import type { Condition } from "./validate-condition";
|
||||||
|
|
||||||
export const ICON_CONDITION: Record<Condition["condition"], string> = {
|
export const ICON_CONDITION: Record<Condition["condition"], string> = {
|
||||||
|
location: mdiMapMarker,
|
||||||
numeric_state: mdiNumeric,
|
numeric_state: mdiNumeric,
|
||||||
state: mdiStateMachine,
|
state: mdiStateMachine,
|
||||||
screen: mdiResponsive,
|
screen: mdiResponsive,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { ensureArray } from "../../../common/array/ensure-array";
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
import type { MediaQueriesListener } from "../../../common/dom/media_query";
|
import type { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
import { listenMediaQuery } from "../../../common/dom/media_query";
|
import { listenMediaQuery } from "../../../common/dom/media_query";
|
||||||
|
|
||||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||||
import { UNKNOWN } from "../../../data/entity";
|
import { UNKNOWN } from "../../../data/entity";
|
||||||
|
import { getUserPerson } from "../../../data/person";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
export type Condition =
|
export type Condition =
|
||||||
|
| LocationCondition
|
||||||
| NumericStateCondition
|
| NumericStateCondition
|
||||||
| StateCondition
|
| StateCondition
|
||||||
| ScreenCondition
|
| ScreenCondition
|
||||||
@@ -25,6 +28,11 @@ interface BaseCondition {
|
|||||||
condition: string;
|
condition: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LocationCondition extends BaseCondition {
|
||||||
|
condition: "location";
|
||||||
|
locations?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface NumericStateCondition extends BaseCondition {
|
export interface NumericStateCondition extends BaseCondition {
|
||||||
condition: "numeric_state";
|
condition: "numeric_state";
|
||||||
entity?: string;
|
entity?: string;
|
||||||
@@ -144,6 +152,17 @@ function checkScreenCondition(condition: ScreenCondition, _: HomeAssistant) {
|
|||||||
: false;
|
: 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) {
|
function checkUserCondition(condition: UserCondition, hass: HomeAssistant) {
|
||||||
return condition.users && hass.user?.id
|
return condition.users && hass.user?.id
|
||||||
? condition.users.includes(hass.user.id)
|
? condition.users.includes(hass.user.id)
|
||||||
@@ -182,6 +201,8 @@ export function checkConditionsMet(
|
|||||||
return checkScreenCondition(c, hass);
|
return checkScreenCondition(c, hass);
|
||||||
case "user":
|
case "user":
|
||||||
return checkUserCondition(c, hass);
|
return checkUserCondition(c, hass);
|
||||||
|
case "location":
|
||||||
|
return checkLocationCondition(c, hass);
|
||||||
case "numeric_state":
|
case "numeric_state":
|
||||||
return checkStateNumericCondition(c, hass);
|
return checkStateNumericCondition(c, hass);
|
||||||
case "and":
|
case "and":
|
||||||
@@ -256,6 +277,10 @@ function validateUserCondition(condition: UserCondition) {
|
|||||||
return condition.users != null;
|
return condition.users != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateLocationCondition(condition: LocationCondition) {
|
||||||
|
return condition.locations != null;
|
||||||
|
}
|
||||||
|
|
||||||
function validateAndCondition(condition: AndCondition) {
|
function validateAndCondition(condition: AndCondition) {
|
||||||
return condition.conditions != null;
|
return condition.conditions != null;
|
||||||
}
|
}
|
||||||
@@ -289,6 +314,8 @@ export function validateConditionalConfig(
|
|||||||
return validateScreenCondition(c);
|
return validateScreenCondition(c);
|
||||||
case "user":
|
case "user":
|
||||||
return validateUserCondition(c);
|
return validateUserCondition(c);
|
||||||
|
case "location":
|
||||||
|
return validateLocationCondition(c);
|
||||||
case "numeric_state":
|
case "numeric_state":
|
||||||
return validateNumericStateCondition(c);
|
return validateNumericStateCondition(c);
|
||||||
case "and":
|
case "and":
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import "./ha-card-condition-editor";
|
|||||||
import type { HaCardConditionEditor } from "./ha-card-condition-editor";
|
import type { HaCardConditionEditor } from "./ha-card-condition-editor";
|
||||||
import type { LovelaceConditionEditorConstructor } from "./types";
|
import type { LovelaceConditionEditorConstructor } from "./types";
|
||||||
import "./types/ha-card-condition-and";
|
import "./types/ha-card-condition-and";
|
||||||
|
import "./types/ha-card-condition-location";
|
||||||
import "./types/ha-card-condition-not";
|
import "./types/ha-card-condition-not";
|
||||||
import "./types/ha-card-condition-numeric_state";
|
import "./types/ha-card-condition-numeric_state";
|
||||||
import "./types/ha-card-condition-or";
|
import "./types/ha-card-condition-or";
|
||||||
@@ -26,6 +27,7 @@ import "./types/ha-card-condition-state";
|
|||||||
import "./types/ha-card-condition-user";
|
import "./types/ha-card-condition-user";
|
||||||
|
|
||||||
const UI_CONDITION = [
|
const UI_CONDITION = [
|
||||||
|
"location",
|
||||||
"numeric_state",
|
"numeric_state",
|
||||||
"state",
|
"state",
|
||||||
"screen",
|
"screen",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7301,6 +7301,11 @@
|
|||||||
"state_equal": "State is equal to",
|
"state_equal": "State is equal to",
|
||||||
"state_not_equal": "State is not 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": {
|
"user": {
|
||||||
"label": "User"
|
"label": "User"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user