From a02b817d7f159412d3153547a4450aeaecc78829 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 May 2022 14:32:11 -0500 Subject: [PATCH] Use new localized context state and source in logbook (#12742) --- src/data/logbook.ts | 140 +++++++----- src/panels/logbook/ha-logbook-renderer.ts | 262 ++++++++++++++-------- src/translations/en.json | 19 +- 3 files changed, 263 insertions(+), 158 deletions(-) diff --git a/src/data/logbook.ts b/src/data/logbook.ts index 578be0dfbb..d6b8ed08c2 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -1,6 +1,10 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { + BINARY_STATE_OFF, + BINARY_STATE_ON, + DOMAINS_WITH_DYNAMIC_PICTURE, +} from "../common/const"; import { computeDomain } from "../common/entity/compute_domain"; -import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const"; import { computeStateDisplay } from "../common/entity/compute_state_display"; import { LocalizeFunc } from "../common/translations/localize"; import { HomeAssistant } from "../types"; @@ -10,26 +14,43 @@ const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages"; export const CONTINUOUS_DOMAINS = ["proximity", "sensor"]; export interface LogbookEntry { - // Python timestamp. Do *1000 to get JS timestamp. - when: number; + // Base data + when: number; // Python timestamp. Do *1000 to get JS timestamp. name: string; message?: string; entity_id?: string; icon?: string; - source?: string; + source?: string; // The trigger source domain?: string; + state?: string; // The state of the entity + // Context data context_id?: string; context_user_id?: string; context_event_type?: string; context_domain?: string; - context_service?: string; + context_service?: string; // Service calls only context_entity_id?: string; - context_entity_id_name?: string; + context_entity_id_name?: string; // Legacy, not longer sent context_name?: string; + context_state?: string; // The state of the entity + context_source?: string; // The trigger source context_message?: string; - state?: string; } +// +// Localization mapping for all the triggers in core +// in homeassistant.components.homeassistant.triggers +// +const triggerPhrases = { + "numeric state of": "triggered_by_numeric_state_of", // number state trigger + "state of": "triggered_by_state_of", // state trigger + event: "triggered_by_event", // event trigger + time: "triggered_by_time", // time trigger + "time pattern": "triggered_by_time_pattern", // time trigger + "Home Assistant stopping": "triggered_by_homeassistant_stopping", // stop event + "Home Assistant starting": "triggered_by_homeassistant_starting", // start event +}; + const DATA_CACHE: { [cacheKey: string]: { [entityId: string]: Promise }; } = {}; @@ -39,17 +60,13 @@ export const getLogbookDataForContext = async ( startDate: string, contextId?: string ): Promise => { - const localize = await hass.loadBackendTranslation("device_class"); - return addLogbookMessage( + await hass.loadBackendTranslation("device_class"); + return getLogbookDataFromServer( hass, - localize, - await getLogbookDataFromServer( - hass, - startDate, - undefined, - undefined, - contextId - ) + startDate, + undefined, + undefined, + contextId ); }; @@ -60,42 +77,17 @@ export const getLogbookData = async ( entityIds?: string[], deviceIds?: string[] ): Promise => { - const localize = await hass.loadBackendTranslation("device_class"); - return addLogbookMessage( - hass, - localize, - // bypass cache if we have a device ID - deviceIds?.length - ? await getLogbookDataFromServer( - hass, - startDate, - endDate, - entityIds, - undefined, - deviceIds - ) - : await getLogbookDataCache(hass, startDate, endDate, entityIds) - ); -}; - -const addLogbookMessage = ( - hass: HomeAssistant, - localize: LocalizeFunc, - logbookData: LogbookEntry[] -): LogbookEntry[] => { - for (const entry of logbookData) { - const stateObj = hass!.states[entry.entity_id!]; - if (entry.state && stateObj) { - entry.message = getLogbookMessage( + await hass.loadBackendTranslation("device_class"); + return deviceIds?.length + ? getLogbookDataFromServer( hass, - localize, - entry.state, - stateObj, - computeDomain(entry.entity_id!) - ); - } - } - return logbookData; + startDate, + endDate, + entityIds, + undefined, + deviceIds + ) + : getLogbookDataCache(hass, startDate, endDate, entityIds); }; const getLogbookDataCache = async ( @@ -204,7 +196,49 @@ export const clearLogbookCache = (startDate: string, endDate: string) => { DATA_CACHE[`${startDate}${endDate}`] = {}; }; -const getLogbookMessage = ( +export const createHistoricState = ( + currentStateObj: HassEntity, + state?: string +): HassEntity => ({ + entity_id: currentStateObj.entity_id, + state: state, + attributes: { + // Rebuild the historical state by copying static attributes only + device_class: currentStateObj?.attributes.device_class, + source_type: currentStateObj?.attributes.source_type, + has_date: currentStateObj?.attributes.has_date, + has_time: currentStateObj?.attributes.has_time, + // We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering, + // as they would present a false state in the log (played media right now vs actual historic data). + entity_picture_local: DOMAINS_WITH_DYNAMIC_PICTURE.has( + computeDomain(currentStateObj.entity_id) + ) + ? undefined + : currentStateObj?.attributes.entity_picture_local, + entity_picture: DOMAINS_WITH_DYNAMIC_PICTURE.has( + computeDomain(currentStateObj.entity_id) + ) + ? undefined + : currentStateObj?.attributes.entity_picture, + }, + }); + +export const localizeTriggerSource = ( + localize: LocalizeFunc, + source: string +) => { + for (const triggerPhrase in triggerPhrases) { + if (source.startsWith(triggerPhrase)) { + return source.replace( + triggerPhrase, + `${localize(`ui.components.logbook.${triggerPhrases[triggerPhrase]}`)}` + ); + } + } + return source; +}; + +export const localizeStateMessage = ( hass: HomeAssistant, localize: LocalizeFunc, state: string, diff --git a/src/panels/logbook/ha-logbook-renderer.ts b/src/panels/logbook/ha-logbook-renderer.ts index e2baefb672..69b7bed3cf 100644 --- a/src/panels/logbook/ha-logbook-renderer.ts +++ b/src/panels/logbook/ha-logbook-renderer.ts @@ -10,7 +10,6 @@ import { import type { HassEntity } from "home-assistant-js-websocket"; import { customElement, eventOptions, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { DOMAINS_WITH_DYNAMIC_PICTURE } from "../../common/const"; import { formatDate } from "../../common/datetime/format_date"; import { formatTimeWithSeconds } from "../../common/datetime/format_time"; import { restoreScroll } from "../../common/decorators/restore-scroll"; @@ -21,7 +20,12 @@ import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl"; import "../../components/entity/state-badge"; import "../../components/ha-circular-progress"; import "../../components/ha-relative-time"; -import { LogbookEntry } from "../../data/logbook"; +import { + createHistoricState, + localizeTriggerSource, + localizeStateMessage, + LogbookEntry, +} from "../../data/logbook"; import { TraceContexts } from "../../data/trace"; import { haStyle, @@ -31,9 +35,12 @@ import { import { HomeAssistant } from "../../types"; import { brandsUrl } from "../../util/brands-url"; -const EVENT_LOCALIZE_MAP = { - script_started: "from_script", -}; +const triggerDomains = ["script", "automation"]; + +const hasContext = (item: LogbookEntry) => + item.context_event_type || item.context_state || item.context_message; +const stripEntityId = (message: string, entityId?: string) => + entityId ? message.replace(entityId, " ") : message; @customElement("ha-logbook-renderer") class HaLogbookRenderer extends LitElement { @@ -128,40 +135,22 @@ class HaLogbookRenderer extends LitElement { if (!item || index === undefined) { return html``; } - - const seenEntityIds: string[] = []; const previous = this.entries[index - 1]; + const seenEntityIds: string[] = []; const currentStateObj = item.entity_id ? this.hass.states[item.entity_id] : undefined; - const item_username = - item.context_user_id && this.userIdToName[item.context_user_id]; + const historicStateObj = currentStateObj + ? createHistoricState(currentStateObj, item.state!) + : undefined; const domain = item.entity_id ? computeDomain(item.entity_id) : // Domain is there if there is no entity ID. item.domain!; - const historicStateObj = item.entity_id ? ({ - entity_id: item.entity_id, - state: item.state, - attributes: { - // Rebuild the historical state by copying static attributes only - device_class: currentStateObj?.attributes.device_class, - source_type: currentStateObj?.attributes.source_type, - has_date: currentStateObj?.attributes.has_date, - has_time: currentStateObj?.attributes.has_time, - // We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering, - // as they would present a false state in the log (played media right now vs actual historic data). - entity_picture_local: DOMAINS_WITH_DYNAMIC_PICTURE.has(domain) - ? undefined - : currentStateObj?.attributes.entity_picture_local, - entity_picture: DOMAINS_WITH_DYNAMIC_PICTURE.has(domain) - ? undefined - : currentStateObj?.attributes.entity_picture, - }, - }) : undefined; const overrideImage = !historicStateObj && !item.icon && + !item.state && domain && isComponentLoaded(this.hass, domain) ? brandsUrl({ @@ -204,45 +193,13 @@ class HaLogbookRenderer extends LitElement { ${!this.noName // Used for more-info panel (single entity case) ? this._renderEntity(item.entity_id, item.name) : ""} - ${item.message - ? html`${this._formatMessageWithPossibleEntity( - item.message, - seenEntityIds, - item.entity_id - )}` - : item.source - ? html` ${this._formatMessageWithPossibleEntity( - item.source, - seenEntityIds, - undefined, - "ui.components.logbook.by" - )}` - : ""} - ${item_username - ? ` ${this.hass.localize( - "ui.components.logbook.by_user" - )} ${item_username}` - : ``} - ${item.context_event_type - ? this._formatEventBy(item, seenEntityIds) - : ""} - ${item.context_message - ? html` ${this._formatMessageWithPossibleEntity( - item.context_message, - seenEntityIds, - item.context_entity_id, - "ui.components.logbook.for" - )}` - : ""} - ${item.context_entity_id && - !seenEntityIds.includes(item.context_entity_id) - ? // Another entity such as an automation or script - html` ${this.hass.localize("ui.components.logbook.for")} - ${this._renderEntity( - item.context_entity_id, - item.context_entity_id_name - )}` - : ""} + ${this._renderMessage( + item, + seenEntityIds, + domain, + historicStateObj + )} + ${this._renderContextMessage(item, seenEntityIds)}
- ${["script", "automation"].includes(item.domain!) && + ${item.context_user_id ? html`${this._renderUser(item)}` : ""} + ${triggerDomains.includes(item.domain!) && item.context_id! in this.traceContexts ? html` - @@ -294,38 +252,149 @@ class HaLogbookRenderer extends LitElement { this._savedScrollPos = (e.target as HTMLDivElement).scrollTop; } - private _formatEventBy(item: LogbookEntry, seenEntities: string[]) { - if (item.context_event_type === "call_service") { - return `${this.hass.localize("ui.components.logbook.from_service")} ${ - item.context_domain - }.${item.context_service}`; + private _renderMessage( + item: LogbookEntry, + seenEntityIds: string[], + domain?: string, + historicStateObj?: HassEntity + ) { + if (item.entity_id) { + if (item.state) { + return historicStateObj + ? localizeStateMessage( + this.hass, + this.hass.localize, + item.state, + historicStateObj, + domain! + ) + : item.state; + } } - if (item.context_event_type === "automation_triggered") { - if (seenEntities.includes(item.context_entity_id!)) { + + const itemHasContext = hasContext(item); + let message = item.message; + if (triggerDomains.includes(domain!) && item.source) { + if (itemHasContext) { + // These domains include the trigger source in the message + // but if we have the context we want to display that instead + // as otherwise we display duplicate triggers return ""; } - seenEntities.push(item.context_entity_id!); - return html`${this.hass.localize("ui.components.logbook.from_automation")} - ${this._renderEntity(item.context_entity_id, item.context_name)}`; + message = localizeTriggerSource(this.hass.localize, item.source); } - if (item.context_name) { - return `${this.hass.localize("ui.components.logbook.from")} ${ - item.context_name - }`; + return message + ? this._formatMessageWithPossibleEntity( + itemHasContext + ? stripEntityId(message, item.context_entity_id) + : message, + seenEntityIds, + undefined + ) + : ""; + } + + private _renderUser(item: LogbookEntry) { + const item_username = + item.context_user_id && this.userIdToName[item.context_user_id]; + if (item_username) { + return `- ${item_username}`; } - if (item.context_event_type === "state_changed") { + return ""; + } + + private _renderUnseenContextSourceEntity( + item: LogbookEntry, + seenEntityIds: string[] + ) { + if ( + !item.context_entity_id || + seenEntityIds.includes(item.context_entity_id!) + ) { return ""; } - if (item.context_event_type! in EVENT_LOCALIZE_MAP) { - return `${this.hass.localize( - `ui.components.logbook.${EVENT_LOCALIZE_MAP[item.context_event_type!]}` - )}`; + // We don't know what caused this entity + // to be included since its an integration + // described event. + return html` (${this._renderEntity( + item.context_entity_id, + item.context_entity_id_name + )})`; + } + + private _renderContextMessage(item: LogbookEntry, seenEntityIds: string[]) { + // State change + if (item.context_state) { + const historicStateObj = + item.context_entity_id && item.context_entity_id in this.hass.states + ? createHistoricState( + this.hass.states[item.context_entity_id], + item.context_state + ) + : undefined; + return html`${this.hass.localize( + "ui.components.logbook.triggered_by_state_of" + )} + ${this._renderEntity(item.context_entity_id, item.context_entity_id_name)} + ${historicStateObj + ? localizeStateMessage( + this.hass, + this.hass.localize, + item.context_state, + historicStateObj, + computeDomain(item.context_entity_id!) + ) + : item.context_state}`; } - return `${this.hass.localize( - "ui.components.logbook.from" - )} ${this.hass.localize("ui.components.logbook.event")} ${ - item.context_event_type - }`; + // Service call + if (item.context_event_type === "call_service") { + return html`${this.hass.localize( + "ui.components.logbook.triggered_by_service" + )} + ${item.context_domain}.${item.context_service}`; + } + if ( + !item.context_message || + seenEntityIds.includes(item.context_entity_id!) + ) { + return ""; + } + // Automation or script + if ( + item.context_event_type === "automation_triggered" || + item.context_event_type === "script_started" + ) { + // context_source is available in 2022.6 and later + const triggerMsg = item.context_source + ? item.context_source + : item.context_message.replace("triggered by ", ""); + const contextTriggerSource = localizeTriggerSource( + this.hass.localize, + triggerMsg + ); + return html`${this.hass.localize( + item.context_event_type === "automation_triggered" + ? "ui.components.logbook.triggered_by_automation" + : "ui.components.logbook.triggered_by_script" + )} + ${this._renderEntity(item.context_entity_id, item.context_entity_id_name)} + ${item.context_message + ? this._formatMessageWithPossibleEntity( + contextTriggerSource, + seenEntityIds + ) + : ""}`; + } + // Generic externally described logbook platform + // These are not localizable + return html` ${this.hass.localize("ui.components.logbook.triggered_by")} + ${item.context_name} + ${this._formatMessageWithPossibleEntity( + item.context_message, + seenEntityIds, + item.context_entity_id + )} + ${this._renderUnseenContextSourceEntity(item, seenEntityIds)}`; } private _renderEntity( @@ -353,8 +422,7 @@ class HaLogbookRenderer extends LitElement { private _formatMessageWithPossibleEntity( message: string, seenEntities: string[], - possibleEntity?: string, - localizePrefix?: string + possibleEntity?: string ) { // // As we are looking at a log(book), we are doing entity_id @@ -376,7 +444,7 @@ class HaLogbookRenderer extends LitElement { seenEntities.push(entityId); const messageEnd = messageParts.splice(i); messageEnd.shift(); // remove the entity - return html` ${messageParts.join(" ")} + return html`${messageParts.join(" ")} ${this._renderEntity( entityId, this.hass.states[entityId].attributes.friendly_name @@ -404,8 +472,8 @@ class HaLogbookRenderer extends LitElement { 0, message.length - possibleEntityName.length ); - return html` ${localizePrefix ? this.hass.localize(localizePrefix) : ""} - ${message} ${this._renderEntity(possibleEntity, possibleEntityName)}`; + return html`${message} + ${this._renderEntity(possibleEntity, possibleEntityName)}`; } } return message; diff --git a/src/translations/en.json b/src/translations/en.json index 9264733ebe..08a5a407f8 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -344,14 +344,17 @@ }, "logbook": { "entries_not_found": "No logbook events found.", - "by_user": "by user", - "by": "by", - "from": "from", - "for": "for", - "event": "event", - "from_service": "from service", - "from_automation": "from automation", - "from_script": "from script", + "triggered_by": "triggered by", + "triggered_by_automation": "triggered by automation", + "triggered_by_script": "triggered by script", + "triggered_by_service": "triggered by service", + "triggered_by_numeric_state_of": "triggered by numeric state of", + "triggered_by_state_of": "triggered by state of", + "triggered_by_event": "triggered by event", + "triggered_by_time": "triggered by time", + "triggered_by_time_pattern": "triggered by time pattern", + "triggered_by_homeassistant_stopping": "triggered by Home Assistant stopping", + "triggered_by_homeassistant_starting": "triggered by Home Assistant starting", "show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]", "retrieval_error": "Could not load logbook", "messages": {