Add event entity (#17332)

This commit is contained in:
Franck Nijhof 2023-07-21 12:18:32 +02:00 committed by GitHub
parent 3189ef0701
commit 0eebc9095c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 302 additions and 31 deletions

View File

@ -135,6 +135,14 @@ const ENTITIES = [
getEntity("text", "unavailable", "unavailable", { getEntity("text", "unavailable", "unavailable", {
friendly_name: "Message", friendly_name: "Message",
}), }),
getEntity("event", "unavailable", "unavailable", {
friendly_name: "Empty remote",
}),
getEntity("event", "doorbell", "2023-07-17T21:26:11.615+00:00", {
friendly_name: "Doorbell",
device_class: "doorbell",
event_type: "Ding-Dong",
}),
]; ];
const CONFIGS = [ const CONFIGS = [
@ -154,6 +162,7 @@ const CONFIGS = [
- input_number.number - input_number.number
- sensor.humidity - sensor.humidity
- text.message - text.message
- event.doorbell
`, `,
}, },
{ {
@ -246,6 +255,7 @@ const CONFIGS = [
- input_number.unavailable - input_number.unavailable
- input_select.unavailable - input_select.unavailable
- text.unavailable - text.unavailable
- event.unavailable
`, `,
}, },
{ {

View File

@ -10,6 +10,7 @@ import {
mdiBookmark, mdiBookmark,
mdiBrightness5, mdiBrightness5,
mdiBullhorn, mdiBullhorn,
mdiButtonPointer,
mdiCalendar, mdiCalendar,
mdiCalendarClock, mdiCalendarClock,
mdiCarCoolantLevel, mdiCarCoolantLevel,
@ -28,7 +29,6 @@ import {
mdiFormatListBulleted, mdiFormatListBulleted,
mdiFormTextbox, mdiFormTextbox,
mdiGauge, mdiGauge,
mdiGestureTapButton,
mdiGoogleAssistant, mdiGoogleAssistant,
mdiGoogleCirclesCommunities, mdiGoogleCirclesCommunities,
mdiHomeAssistant, mdiHomeAssistant,
@ -93,7 +93,7 @@ export const FIXED_DOMAIN_ICONS = {
homekit: mdiHomeAutomation, homekit: mdiHomeAutomation,
image: mdiImage, image: mdiImage,
image_processing: mdiImageFilterFrames, image_processing: mdiImageFilterFrames,
input_button: mdiGestureTapButton, input_button: mdiButtonPointer,
input_datetime: mdiCalendarClock, input_datetime: mdiCalendarClock,
input_number: mdiRayVertex, input_number: mdiRayVertex,
input_select: mdiFormatListBulleted, input_select: mdiFormatListBulleted,
@ -178,6 +178,7 @@ export const DOMAINS_WITH_CARD = [
"climate", "climate",
"cover", "cover",
"configurator", "configurator",
"event",
"input_button", "input_button",
"input_select", "input_select",
"input_number", "input_number",

View File

@ -185,9 +185,15 @@ export const computeStateDisplayFromEntityAttributes = (
// state is a timestamp // state is a timestamp
if ( if (
["button", "image", "input_button", "scene", "stt", "tts"].includes( [
domain "button",
) || "event",
"image",
"input_button",
"scene",
"stt",
"tts",
].includes(domain) ||
(domain === "sensor" && attributes.device_class === "timestamp") (domain === "sensor" && attributes.device_class === "timestamp")
) { ) {
try { try {

View File

@ -7,6 +7,7 @@ import {
mdiAudioVideoOff, mdiAudioVideoOff,
mdiBluetooth, mdiBluetooth,
mdiBluetoothConnect, mdiBluetoothConnect,
mdiButtonPointer,
mdiCalendar, mdiCalendar,
mdiCast, mdiCast,
mdiCastConnected, mdiCastConnected,
@ -16,8 +17,10 @@ import {
mdiClock, mdiClock,
mdiCloseCircleOutline, mdiCloseCircleOutline,
mdiCrosshairsQuestion, mdiCrosshairsQuestion,
mdiDoorbell,
mdiFan, mdiFan,
mdiFanOff, mdiFanOff,
mdiGestureTap,
mdiGestureTapButton, mdiGestureTapButton,
mdiLanConnect, mdiLanConnect,
mdiLanDisconnect, mdiLanDisconnect,
@ -25,6 +28,7 @@ import {
mdiLockAlert, mdiLockAlert,
mdiLockClock, mdiLockClock,
mdiLockOpen, mdiLockOpen,
mdiMotionSensor,
mdiPackage, mdiPackage,
mdiPackageDown, mdiPackageDown,
mdiPackageUp, mdiPackageUp,
@ -111,7 +115,7 @@ export const domainIconWithoutDefault = (
case "update": case "update":
return mdiPackageUp; return mdiPackageUp;
default: default:
return mdiGestureTapButton; return mdiButtonPointer;
} }
case "camera": case "camera":
@ -131,6 +135,18 @@ export const domainIconWithoutDefault = (
} }
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount; return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
case "event":
switch (stateObj?.attributes.device_class) {
case "doorbell":
return mdiDoorbell;
case "button":
return mdiGestureTapButton;
case "motion":
return mdiMotionSensor;
default:
return mdiGestureTap;
}
case "fan": case "fan":
return compareState === "off" ? mdiFanOff : mdiFan; return compareState === "off" ? mdiFanOff : mdiFan;

View File

@ -250,6 +250,11 @@ export const getStates = (
result.push("home", "not_home"); result.push("home", "not_home");
} }
break; break;
case "event":
if (attribute === "event_type") {
result.push(...state.attributes.event_types);
}
break;
case "fan": case "fan":
if (attribute === "preset_mode") { if (attribute === "preset_mode") {
result.push(...state.attributes.preset_modes); result.push(...state.attributes.preset_modes);

View File

@ -6,7 +6,7 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
const compareState = state !== undefined ? state : stateObj?.state; const compareState = state !== undefined ? state : stateObj?.state;
if (["button", "input_button", "scene"].includes(domain)) { if (["button", "event", "input_button", "scene"].includes(domain)) {
return compareState !== UNAVAILABLE; return compareState !== UNAVAILABLE;
} }

View File

@ -9,6 +9,7 @@ export const STATE_ATTRIBUTES = [
"emulated_hue_name", "emulated_hue_name",
"emulated_hue", "emulated_hue",
"entity_picture", "entity_picture",
"event_types",
"friendly_name", "friendly_name",
"haaska_hidden", "haaska_hidden",
"haaska_name", "haaska_name",

View File

@ -12,6 +12,7 @@ import { LocalizeFunc } from "../common/translations/localize";
import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker"; import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { UNAVAILABLE, UNKNOWN } from "./entity"; import { UNAVAILABLE, UNKNOWN } from "./entity";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages"; const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"]; export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"];
@ -156,6 +157,7 @@ export const createHistoricState = (
attributes: { attributes: {
// Rebuild the historical state by copying static attributes only // Rebuild the historical state by copying static attributes only
device_class: currentStateObj?.attributes.device_class, device_class: currentStateObj?.attributes.device_class,
event_type: currentStateObj?.attributes.event_type,
source_type: currentStateObj?.attributes.source_type, source_type: currentStateObj?.attributes.source_type,
has_date: currentStateObj?.attributes.has_date, has_date: currentStateObj?.attributes.has_date,
has_time: currentStateObj?.attributes.has_time, has_time: currentStateObj?.attributes.has_time,
@ -343,6 +345,23 @@ export const localizeStateMessage = (
} }
break; break;
case "event": {
const event_type =
computeAttributeValueDisplay(
hass!.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
"event_type"
)?.toString() ||
localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event`, {
event_type: autoCaseNoun(event_type, hass.language),
});
}
case "lock": case "lock":
switch (state) { switch (state) {
case "unlocked": case "unlocked":

View File

@ -10,6 +10,7 @@ export const SCENE_IGNORED_DOMAINS = [
"button", "button",
"configuration", "configuration",
"device_tracker", "device_tracker",
"event",
"image_processing", "image_processing",
"input_button", "input_button",
"persistent_notification", "persistent_notification",

View File

@ -72,6 +72,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"select", "select",
"text", "text",
"update", "update",
"event",
]; ];
/** Domains that should have the history hidden in the more info dialog. */ /** Domains that should have the history hidden in the more info dialog. */

View File

@ -52,6 +52,8 @@ export default class HaNumericStateCondition extends LitElement {
"effect_list", "effect_list",
"effect", "effect",
"entity_picture", "entity_picture",
"event_type",
"event_types",
"fan_mode", "fan_mode",
"fan_modes", "fan_modes",
"fan_speed_list", "fan_speed_list",

View File

@ -42,6 +42,7 @@ const SCHEMA = [
"editable", "editable",
"effect_list", "effect_list",
"entity_picture", "entity_picture",
"event_types",
"fan_modes", "fan_modes",
"fan_speed_list", "fan_speed_list",
"forecast", "forecast",

View File

@ -51,6 +51,8 @@ export class HaNumericStateTrigger extends LitElement {
"effect", "effect",
"entity_id", "entity_id",
"entity_picture", "entity_picture",
"event_type",
"event_types",
"fan_mode", "fan_mode",
"fan_modes", "fan_modes",
"fan_speed_list", "fan_speed_list",

View File

@ -76,6 +76,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
"effect_list", "effect_list",
"entity_id", "entity_id",
"entity_picture", "entity_picture",
"event_types",
"fan_modes", "fan_modes",
"fan_speed_list", "fan_speed_list",
"friendly_name", "friendly_name",

View File

@ -207,16 +207,25 @@ export class HaConfigDevicePage extends LitElement {
const result = groupBy(entities, (entry) => const result = groupBy(entities, (entry) =>
entry.entity_category entry.entity_category
? entry.entity_category ? entry.entity_category
: computeDomain(entry.entity_id) === "event"
? "event"
: SENSOR_ENTITIES.includes(computeDomain(entry.entity_id)) : SENSOR_ENTITIES.includes(computeDomain(entry.entity_id))
? "sensor" ? "sensor"
: "control" : "control"
) as Record< ) as Record<
| "control" | "control"
| "event"
| "sensor" | "sensor"
| NonNullable<EntityRegistryEntry["entity_category"]>, | NonNullable<EntityRegistryEntry["entity_category"]>,
EntityRegistryStateEntry[] EntityRegistryStateEntry[]
>; >;
for (const key of ["control", "sensor", "diagnostic", "config"]) { for (const key of [
"config",
"control",
"diagnostic",
"event",
"sensor",
]) {
if (!(key in result)) { if (!(key in result)) {
result[key] = []; result[key] = [];
} }
@ -877,8 +886,9 @@ export class HaConfigDevicePage extends LitElement {
${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""} ${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""}
</div> </div>
<div class="column"> <div class="column">
${(["control", "sensor", "config", "diagnostic"] as const).map( ${(
(category) => ["control", "sensor", "event", "config", "diagnostic"] as const
).map((category) =>
// Make sure we render controls if no other cards will be rendered // Make sure we render controls if no other cards will be rendered
entitiesByCategory[category].length > 0 || entitiesByCategory[category].length > 0 ||
(entities.length === 0 && category === "control") (entities.length === 0 && category === "control")

View File

@ -1,3 +1,4 @@
import "../entity-rows/hui-event-entity-row";
import "../entity-rows/hui-media-player-entity-row"; import "../entity-rows/hui-media-player-entity-row";
import "../entity-rows/hui-scene-entity-row"; import "../entity-rows/hui-scene-entity-row";
import "../entity-rows/hui-script-entity-row"; import "../entity-rows/hui-script-entity-row";
@ -29,6 +30,7 @@ const LAZY_LOAD_TYPES = {
"cover-entity": () => import("../entity-rows/hui-cover-entity-row"), "cover-entity": () => import("../entity-rows/hui-cover-entity-row"),
"date-entity": () => import("../entity-rows/hui-date-entity-row"), "date-entity": () => import("../entity-rows/hui-date-entity-row"),
"datetime-entity": () => import("../entity-rows/hui-datetime-entity-row"), "datetime-entity": () => import("../entity-rows/hui-datetime-entity-row"),
"event-entity": () => import("../entity-rows/hui-event-entity-row"),
"group-entity": () => import("../entity-rows/hui-group-entity-row"), "group-entity": () => import("../entity-rows/hui-group-entity-row"),
"input-button-entity": () => "input-button-entity": () =>
import("../entity-rows/hui-input-button-entity-row"), import("../entity-rows/hui-input-button-entity-row"),
@ -65,6 +67,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
cover: "cover", cover: "cover",
date: "date", date: "date",
datetime: "datetime", datetime: "datetime",
event: "event",
fan: "toggle", fan: "toggle",
group: "group", group: "group",
humidifier: "humidifier", humidifier: "humidifier",

View File

@ -0,0 +1,129 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { isUnavailableState } from "../../../data/entity";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { EntitiesCardEntityConfig } from "../cards/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import "../components/hui-timestamp-display";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { TimestampRenderingFormat } from "../components/types";
import { LovelaceRow } from "./types";
interface EventEntityConfig extends EntitiesCardEntityConfig {
format?: TimestampRenderingFormat;
}
@customElement("hui-event-entity-row")
class HuiEventEntityRow extends LitElement implements LovelaceRow {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EventEntityConfig;
public setConfig(config: EventEntityConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps);
}
protected render() {
if (!this._config || !this.hass) {
return nothing;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
<hui-warning>
${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning>
`;
}
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<div
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config.hold_action),
hasDoubleClick: hasAction(this._config.double_tap_action),
})}
>
<div class="what">
${isUnavailableState(stateObj.state)
? computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
)
: computeAttributeValueDisplay(
this.hass!.localize,
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"event_type"
)}
</div>
<div class="when">
${isUnavailableState(stateObj.state)
? ``
: html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
.format=${this._config.format}
capitalize
></hui-timestamp-display>
`}
</div>
</div>
</hui-generic-entity-row>
`;
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action);
}
static get styles(): CSSResultGroup {
return css`
div {
text-align: right;
}
.when {
color: var(--secondary-text-color);
}
.what {
color: var(--primary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-event-entity-row": HuiEventEntityRow;
}
}

View File

@ -8,6 +8,7 @@ import "./state-card-humidifier";
import "./state-card-configurator"; import "./state-card-configurator";
import "./state-card-cover"; import "./state-card-cover";
import "./state-card-display"; import "./state-card-display";
import "./state-card-event";
import "./state-card-input_button"; import "./state-card-input_button";
import "./state-card-input_number"; import "./state-card-input_number";
import "./state-card-input_select"; import "./state-card-input_select";

View File

@ -0,0 +1,59 @@
import { HassEntity } from "home-assistant-js-websocket";
import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/entity/ha-entity-toggle";
import "../components/entity/state-info";
import { HomeAssistant } from "../types";
import { isUnavailableState } from "../data/entity";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { haStyle } from "../resources/styles";
@customElement("state-card-event")
export class StateCardEvent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public stateObj!: HassEntity;
@property({ type: Boolean }) public inDialog = false;
protected render() {
return html`
<div class="horizontal justified layout">
<state-info
.hass=${this.hass}
.stateObj=${this.stateObj}
.inDialog=${this.inDialog}
></state-info>
<div class="event_type">
${isUnavailableState(this.stateObj.state)
? computeStateDisplay(
this.hass!.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
)
: computeAttributeValueDisplay(
this.hass!.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"event_type"
)}
</div>
</div>
`;
}
static get styles(): CSSResultGroup {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"state-card-event": StateCardEvent;
}
}

View File

@ -357,7 +357,9 @@
"became_unavailable": "became unavailable", "became_unavailable": "became unavailable",
"became_unknown": "became unknown", "became_unknown": "became unknown",
"detected_tampering": "detected tampering", "detected_tampering": "detected tampering",
"cleared_tampering": "cleared tampering" "cleared_tampering": "cleared tampering",
"detected_event": "{event_type} event detected",
"detected_unknown_event": "detected an unknown event"
} }
}, },
"entity": { "entity": {
@ -2322,7 +2324,7 @@
} }
}, },
"event": { "event": {
"label": "Event", "label": "Manual event",
"event_type": "Event type", "event_type": "Event type",
"event_data": "Event data", "event_data": "Event data",
"context_users": "Limit to events triggered by", "context_users": "Limit to events triggered by",
@ -2640,11 +2642,11 @@
"label": "Condition" "label": "Condition"
}, },
"event": { "event": {
"label": "Event", "label": "Manual event",
"event": "[%key:ui::panel::config::automation::editor::triggers::type::event::label%]", "event": "[%key:ui::panel::config::automation::editor::triggers::type::event::event_type%]",
"event_data": "[%key:ui::panel::config::automation::editor::triggers::type::event::event_data%]", "event_data": "[%key:ui::panel::config::automation::editor::triggers::type::event::event_data%]",
"description": { "description": {
"full": "Fire event {name}", "full": "Manually fire event {name}",
"template": "based on a template" "template": "based on a template"
} }
}, },
@ -3212,6 +3214,7 @@
"entities": { "entities": {
"entities": "Entities", "entities": "Entities",
"control": "Controls", "control": "Controls",
"event": "Events",
"sensor": "Sensors", "sensor": "Sensors",
"diagnostic": "Diagnostic", "diagnostic": "Diagnostic",
"config": "Configuration", "config": "Configuration",