diff --git a/gallery/src/pages/lovelace/entities-card.ts b/gallery/src/pages/lovelace/entities-card.ts index 075fa73351..aa6368878d 100644 --- a/gallery/src/pages/lovelace/entities-card.ts +++ b/gallery/src/pages/lovelace/entities-card.ts @@ -135,6 +135,14 @@ const ENTITIES = [ getEntity("text", "unavailable", "unavailable", { 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 = [ @@ -154,6 +162,7 @@ const CONFIGS = [ - input_number.number - sensor.humidity - text.message + - event.doorbell `, }, { @@ -246,6 +255,7 @@ const CONFIGS = [ - input_number.unavailable - input_select.unavailable - text.unavailable + - event.unavailable `, }, { diff --git a/src/common/const.ts b/src/common/const.ts index b4f79e5126..7902662135 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -10,6 +10,7 @@ import { mdiBookmark, mdiBrightness5, mdiBullhorn, + mdiButtonPointer, mdiCalendar, mdiCalendarClock, mdiCarCoolantLevel, @@ -28,7 +29,6 @@ import { mdiFormatListBulleted, mdiFormTextbox, mdiGauge, - mdiGestureTapButton, mdiGoogleAssistant, mdiGoogleCirclesCommunities, mdiHomeAssistant, @@ -93,7 +93,7 @@ export const FIXED_DOMAIN_ICONS = { homekit: mdiHomeAutomation, image: mdiImage, image_processing: mdiImageFilterFrames, - input_button: mdiGestureTapButton, + input_button: mdiButtonPointer, input_datetime: mdiCalendarClock, input_number: mdiRayVertex, input_select: mdiFormatListBulleted, @@ -178,6 +178,7 @@ export const DOMAINS_WITH_CARD = [ "climate", "cover", "configurator", + "event", "input_button", "input_select", "input_number", diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 998949b007..4baad8357d 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -185,9 +185,15 @@ export const computeStateDisplayFromEntityAttributes = ( // state is a timestamp 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") ) { try { diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index 3ced61b3f8..eeefc4598c 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -7,6 +7,7 @@ import { mdiAudioVideoOff, mdiBluetooth, mdiBluetoothConnect, + mdiButtonPointer, mdiCalendar, mdiCast, mdiCastConnected, @@ -16,8 +17,10 @@ import { mdiClock, mdiCloseCircleOutline, mdiCrosshairsQuestion, + mdiDoorbell, mdiFan, mdiFanOff, + mdiGestureTap, mdiGestureTapButton, mdiLanConnect, mdiLanDisconnect, @@ -25,6 +28,7 @@ import { mdiLockAlert, mdiLockClock, mdiLockOpen, + mdiMotionSensor, mdiPackage, mdiPackageDown, mdiPackageUp, @@ -111,7 +115,7 @@ export const domainIconWithoutDefault = ( case "update": return mdiPackageUp; default: - return mdiGestureTapButton; + return mdiButtonPointer; } case "camera": @@ -131,6 +135,18 @@ export const domainIconWithoutDefault = ( } 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": return compareState === "off" ? mdiFanOff : mdiFan; diff --git a/src/common/entity/get_states.ts b/src/common/entity/get_states.ts index f8d10ad99f..7eaf7bfb81 100644 --- a/src/common/entity/get_states.ts +++ b/src/common/entity/get_states.ts @@ -250,6 +250,11 @@ export const getStates = ( result.push("home", "not_home"); } break; + case "event": + if (attribute === "event_type") { + result.push(...state.attributes.event_types); + } + break; case "fan": if (attribute === "preset_mode") { result.push(...state.attributes.preset_modes); diff --git a/src/common/entity/state_active.ts b/src/common/entity/state_active.ts index a22ee5c9e5..34162fcc0b 100644 --- a/src/common/entity/state_active.ts +++ b/src/common/entity/state_active.ts @@ -6,7 +6,7 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean { const domain = computeDomain(stateObj.entity_id); 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; } diff --git a/src/data/entity_attributes.ts b/src/data/entity_attributes.ts index 7e3fae87aa..77dcc0b465 100644 --- a/src/data/entity_attributes.ts +++ b/src/data/entity_attributes.ts @@ -9,6 +9,7 @@ export const STATE_ATTRIBUTES = [ "emulated_hue_name", "emulated_hue", "entity_picture", + "event_types", "friendly_name", "haaska_hidden", "haaska_name", diff --git a/src/data/logbook.ts b/src/data/logbook.ts index 37b0737bea..75deda104e 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -12,6 +12,7 @@ import { LocalizeFunc } from "../common/translations/localize"; import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker"; import { HomeAssistant } from "../types"; import { UNAVAILABLE, UNKNOWN } from "./entity"; +import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display"; const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages"; export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"]; @@ -156,6 +157,7 @@ export const createHistoricState = ( attributes: { // Rebuild the historical state by copying static attributes only device_class: currentStateObj?.attributes.device_class, + event_type: currentStateObj?.attributes.event_type, source_type: currentStateObj?.attributes.source_type, has_date: currentStateObj?.attributes.has_date, has_time: currentStateObj?.attributes.has_time, @@ -343,6 +345,23 @@ export const localizeStateMessage = ( } 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": switch (state) { case "unlocked": diff --git a/src/data/scene.ts b/src/data/scene.ts index e8f27d6c26..b9b6fa4d2c 100644 --- a/src/data/scene.ts +++ b/src/data/scene.ts @@ -10,6 +10,7 @@ export const SCENE_IGNORED_DOMAINS = [ "button", "configuration", "device_tracker", + "event", "image_processing", "input_button", "persistent_notification", diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index d6d6d615aa..a0dc211937 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -72,6 +72,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [ "select", "text", "update", + "event", ]; /** Domains that should have the history hidden in the more info dialog. */ diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts b/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts index a7458813db..ea41ca70d0 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts @@ -52,6 +52,8 @@ export default class HaNumericStateCondition extends LitElement { "effect_list", "effect", "entity_picture", + "event_type", + "event_types", "fan_mode", "fan_modes", "fan_speed_list", diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts index e61438ad3e..407699c47e 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts @@ -42,6 +42,7 @@ const SCHEMA = [ "editable", "effect_list", "entity_picture", + "event_types", "fan_modes", "fan_speed_list", "forecast", diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts index 62315025a4..4f27495023 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts @@ -51,6 +51,8 @@ export class HaNumericStateTrigger extends LitElement { "effect", "entity_id", "entity_picture", + "event_type", + "event_types", "fan_mode", "fan_modes", "fan_speed_list", diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts index 61904a424f..4899fb19ee 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts @@ -76,6 +76,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement { "effect_list", "entity_id", "entity_picture", + "event_types", "fan_modes", "fan_speed_list", "friendly_name", diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 2c2051065e..3c3f513507 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -207,16 +207,25 @@ export class HaConfigDevicePage extends LitElement { const result = groupBy(entities, (entry) => entry.entity_category ? entry.entity_category + : computeDomain(entry.entity_id) === "event" + ? "event" : SENSOR_ENTITIES.includes(computeDomain(entry.entity_id)) ? "sensor" : "control" ) as Record< | "control" + | "event" | "sensor" | NonNullable, EntityRegistryStateEntry[] >; - for (const key of ["control", "sensor", "diagnostic", "config"]) { + for (const key of [ + "config", + "control", + "diagnostic", + "event", + "sensor", + ]) { if (!(key in result)) { result[key] = []; } @@ -877,24 +886,25 @@ export class HaConfigDevicePage extends LitElement { ${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""}
- ${(["control", "sensor", "config", "diagnostic"] as const).map( - (category) => - // Make sure we render controls if no other cards will be rendered - entitiesByCategory[category].length > 0 || - (entities.length === 0 && category === "control") - ? html` - - - ` - : "" + ${( + ["control", "sensor", "event", "config", "diagnostic"] as const + ).map((category) => + // Make sure we render controls if no other cards will be rendered + entitiesByCategory[category].length > 0 || + (entities.length === 0 && category === "control") + ? html` + + + ` + : "" )} import("../entity-rows/hui-cover-entity-row"), "date-entity": () => import("../entity-rows/hui-date-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"), "input-button-entity": () => import("../entity-rows/hui-input-button-entity-row"), @@ -65,6 +67,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { cover: "cover", date: "date", datetime: "datetime", + event: "event", fan: "toggle", group: "group", humidifier: "humidifier", diff --git a/src/panels/lovelace/entity-rows/hui-event-entity-row.ts b/src/panels/lovelace/entity-rows/hui-event-entity-row.ts new file mode 100644 index 0000000000..35c98d1a0d --- /dev/null +++ b/src/panels/lovelace/entity-rows/hui-event-entity-row.ts @@ -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` + + ${createEntityNotFoundWarning(this.hass, this._config.entity)} + + `; + } + + return html` + +
+
+ ${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" + )} +
+
+ ${isUnavailableState(stateObj.state) + ? `` + : html` + + `} +
+
+
+ `; + } + + 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; + } +} diff --git a/src/state-summary/state-card-content.js b/src/state-summary/state-card-content.js index b1669ae994..4171bd447a 100644 --- a/src/state-summary/state-card-content.js +++ b/src/state-summary/state-card-content.js @@ -8,6 +8,7 @@ import "./state-card-humidifier"; import "./state-card-configurator"; import "./state-card-cover"; import "./state-card-display"; +import "./state-card-event"; import "./state-card-input_button"; import "./state-card-input_number"; import "./state-card-input_select"; diff --git a/src/state-summary/state-card-event.ts b/src/state-summary/state-card-event.ts new file mode 100644 index 0000000000..20d590fb22 --- /dev/null +++ b/src/state-summary/state-card-event.ts @@ -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` +
+ +
+ ${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" + )} +
+
+ `; + } + + static get styles(): CSSResultGroup { + return haStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "state-card-event": StateCardEvent; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 4e6590eb7e..cad64bcb46 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -357,7 +357,9 @@ "became_unavailable": "became unavailable", "became_unknown": "became unknown", "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": { @@ -2322,7 +2324,7 @@ } }, "event": { - "label": "Event", + "label": "Manual event", "event_type": "Event type", "event_data": "Event data", "context_users": "Limit to events triggered by", @@ -2640,11 +2642,11 @@ "label": "Condition" }, "event": { - "label": "Event", - "event": "[%key:ui::panel::config::automation::editor::triggers::type::event::label%]", + "label": "Manual event", + "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%]", "description": { - "full": "Fire event {name}", + "full": "Manually fire event {name}", "template": "based on a template" } }, @@ -3212,6 +3214,7 @@ "entities": { "entities": "Entities", "control": "Controls", + "event": "Events", "sensor": "Sensors", "diagnostic": "Diagnostic", "config": "Configuration",