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", {
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
`,
},
{

View File

@ -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",

View File

@ -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 {

View File

@ -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;

View File

@ -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);

View File

@ -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;
}

View File

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

View File

@ -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":

View File

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

View File

@ -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. */

View File

@ -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",

View File

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

View File

@ -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",

View File

@ -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",

View File

@ -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<EntityRegistryEntry["entity_category"]>,
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,8 +886,9 @@ export class HaConfigDevicePage extends LitElement {
${!this.narrow ? [automationCard, sceneCard, scriptCard] : ""}
</div>
<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
entitiesByCategory[category].length > 0 ||
(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-scene-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"),
"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",

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-cover";
import "./state-card-display";
import "./state-card-event";
import "./state-card-input_button";
import "./state-card-input_number";
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_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",