Use new localized context state and source in logbook (#12742)

This commit is contained in:
J. Nick Koston 2022-05-23 14:32:11 -05:00 committed by GitHub
parent 7db6e0b779
commit a02b817d7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 263 additions and 158 deletions

View File

@ -1,6 +1,10 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; 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 { 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 { computeStateDisplay } from "../common/entity/compute_state_display";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
@ -10,26 +14,43 @@ const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["proximity", "sensor"]; export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
export interface LogbookEntry { export interface LogbookEntry {
// Python timestamp. Do *1000 to get JS timestamp. // Base data
when: number; when: number; // Python timestamp. Do *1000 to get JS timestamp.
name: string; name: string;
message?: string; message?: string;
entity_id?: string; entity_id?: string;
icon?: string; icon?: string;
source?: string; source?: string; // The trigger source
domain?: string; domain?: string;
state?: string; // The state of the entity
// Context data
context_id?: string; context_id?: string;
context_user_id?: string; context_user_id?: string;
context_event_type?: string; context_event_type?: string;
context_domain?: string; context_domain?: string;
context_service?: string; context_service?: string; // Service calls only
context_entity_id?: string; context_entity_id?: string;
context_entity_id_name?: string; context_entity_id_name?: string; // Legacy, not longer sent
context_name?: string; context_name?: string;
context_state?: string; // The state of the entity
context_source?: string; // The trigger source
context_message?: string; 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: { const DATA_CACHE: {
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> }; [cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
} = {}; } = {};
@ -39,17 +60,13 @@ export const getLogbookDataForContext = async (
startDate: string, startDate: string,
contextId?: string contextId?: string
): Promise<LogbookEntry[]> => { ): Promise<LogbookEntry[]> => {
const localize = await hass.loadBackendTranslation("device_class"); await hass.loadBackendTranslation("device_class");
return addLogbookMessage( return getLogbookDataFromServer(
hass,
localize,
await getLogbookDataFromServer(
hass, hass,
startDate, startDate,
undefined, undefined,
undefined, undefined,
contextId contextId
)
); );
}; };
@ -60,13 +77,9 @@ export const getLogbookData = async (
entityIds?: string[], entityIds?: string[],
deviceIds?: string[] deviceIds?: string[]
): Promise<LogbookEntry[]> => { ): Promise<LogbookEntry[]> => {
const localize = await hass.loadBackendTranslation("device_class"); await hass.loadBackendTranslation("device_class");
return addLogbookMessage( return deviceIds?.length
hass, ? getLogbookDataFromServer(
localize,
// bypass cache if we have a device ID
deviceIds?.length
? await getLogbookDataFromServer(
hass, hass,
startDate, startDate,
endDate, endDate,
@ -74,28 +87,7 @@ export const getLogbookData = async (
undefined, undefined,
deviceIds deviceIds
) )
: await getLogbookDataCache(hass, startDate, endDate, entityIds) : 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(
hass,
localize,
entry.state,
stateObj,
computeDomain(entry.entity_id!)
);
}
}
return logbookData;
}; };
const getLogbookDataCache = async ( const getLogbookDataCache = async (
@ -204,7 +196,49 @@ export const clearLogbookCache = (startDate: string, endDate: string) => {
DATA_CACHE[`${startDate}${endDate}`] = {}; DATA_CACHE[`${startDate}${endDate}`] = {};
}; };
const getLogbookMessage = ( export const createHistoricState = (
currentStateObj: HassEntity,
state?: string
): HassEntity => <HassEntity>(<unknown>{
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, hass: HomeAssistant,
localize: LocalizeFunc, localize: LocalizeFunc,
state: string, state: string,

View File

@ -10,7 +10,6 @@ import {
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { customElement, eventOptions, property } from "lit/decorators"; import { customElement, eventOptions, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { DOMAINS_WITH_DYNAMIC_PICTURE } from "../../common/const";
import { formatDate } from "../../common/datetime/format_date"; import { formatDate } from "../../common/datetime/format_date";
import { formatTimeWithSeconds } from "../../common/datetime/format_time"; import { formatTimeWithSeconds } from "../../common/datetime/format_time";
import { restoreScroll } from "../../common/decorators/restore-scroll"; 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/entity/state-badge";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-relative-time"; import "../../components/ha-relative-time";
import { LogbookEntry } from "../../data/logbook"; import {
createHistoricState,
localizeTriggerSource,
localizeStateMessage,
LogbookEntry,
} from "../../data/logbook";
import { TraceContexts } from "../../data/trace"; import { TraceContexts } from "../../data/trace";
import { import {
haStyle, haStyle,
@ -31,9 +35,12 @@ import {
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url"; import { brandsUrl } from "../../util/brands-url";
const EVENT_LOCALIZE_MAP = { const triggerDomains = ["script", "automation"];
script_started: "from_script",
}; 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") @customElement("ha-logbook-renderer")
class HaLogbookRenderer extends LitElement { class HaLogbookRenderer extends LitElement {
@ -128,40 +135,22 @@ class HaLogbookRenderer extends LitElement {
if (!item || index === undefined) { if (!item || index === undefined) {
return html``; return html``;
} }
const seenEntityIds: string[] = [];
const previous = this.entries[index - 1]; const previous = this.entries[index - 1];
const seenEntityIds: string[] = [];
const currentStateObj = item.entity_id const currentStateObj = item.entity_id
? this.hass.states[item.entity_id] ? this.hass.states[item.entity_id]
: undefined; : undefined;
const item_username = const historicStateObj = currentStateObj
item.context_user_id && this.userIdToName[item.context_user_id]; ? createHistoricState(currentStateObj, item.state!)
: undefined;
const domain = item.entity_id const domain = item.entity_id
? computeDomain(item.entity_id) ? computeDomain(item.entity_id)
: // Domain is there if there is no entity ID. : // Domain is there if there is no entity ID.
item.domain!; item.domain!;
const historicStateObj = item.entity_id ? <HassEntity>(<unknown>{
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 = const overrideImage =
!historicStateObj && !historicStateObj &&
!item.icon && !item.icon &&
!item.state &&
domain && domain &&
isComponentLoaded(this.hass, domain) isComponentLoaded(this.hass, domain)
? brandsUrl({ ? brandsUrl({
@ -204,45 +193,13 @@ class HaLogbookRenderer extends LitElement {
${!this.noName // Used for more-info panel (single entity case) ${!this.noName // Used for more-info panel (single entity case)
? this._renderEntity(item.entity_id, item.name) ? this._renderEntity(item.entity_id, item.name)
: ""} : ""}
${item.message ${this._renderMessage(
? html`${this._formatMessageWithPossibleEntity( item,
item.message,
seenEntityIds, seenEntityIds,
item.entity_id domain,
)}` historicStateObj
: item.source )}
? html` ${this._formatMessageWithPossibleEntity( ${this._renderContextMessage(item, seenEntityIds)}
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
)}`
: ""}
</div> </div>
<div class="secondary"> <div class="secondary">
<span <span
@ -257,7 +214,8 @@ class HaLogbookRenderer extends LitElement {
.datetime=${item.when * 1000} .datetime=${item.when * 1000}
capitalize capitalize
></ha-relative-time> ></ha-relative-time>
${["script", "automation"].includes(item.domain!) && ${item.context_user_id ? html`${this._renderUser(item)}` : ""}
${triggerDomains.includes(item.domain!) &&
item.context_id! in this.traceContexts item.context_id! in this.traceContexts
? html` ? html`
- -
@ -294,38 +252,149 @@ class HaLogbookRenderer extends LitElement {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop; this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
} }
private _formatEventBy(item: LogbookEntry, seenEntities: string[]) { 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;
}
}
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 "";
}
message = localizeTriggerSource(this.hass.localize, item.source);
}
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}`;
}
return "";
}
private _renderUnseenContextSourceEntity(
item: LogbookEntry,
seenEntityIds: string[]
) {
if (
!item.context_entity_id ||
seenEntityIds.includes(item.context_entity_id!)
) {
return "";
}
// 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}`;
}
// Service call
if (item.context_event_type === "call_service") { if (item.context_event_type === "call_service") {
return `${this.hass.localize("ui.components.logbook.from_service")} ${ return html`${this.hass.localize(
item.context_domain "ui.components.logbook.triggered_by_service"
}.${item.context_service}`; )}
${item.context_domain}.${item.context_service}`;
} }
if (item.context_event_type === "automation_triggered") { if (
if (seenEntities.includes(item.context_entity_id!)) { !item.context_message ||
seenEntityIds.includes(item.context_entity_id!)
) {
return ""; return "";
} }
seenEntities.push(item.context_entity_id!); // Automation or script
return html`${this.hass.localize("ui.components.logbook.from_automation")} if (
${this._renderEntity(item.context_entity_id, item.context_name)}`; 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
)
: ""}`;
} }
if (item.context_name) { // Generic externally described logbook platform
return `${this.hass.localize("ui.components.logbook.from")} ${ // These are not localizable
item.context_name return html` ${this.hass.localize("ui.components.logbook.triggered_by")}
}`; ${item.context_name}
} ${this._formatMessageWithPossibleEntity(
if (item.context_event_type === "state_changed") { item.context_message,
return ""; seenEntityIds,
} item.context_entity_id
if (item.context_event_type! in EVENT_LOCALIZE_MAP) { )}
return `${this.hass.localize( ${this._renderUnseenContextSourceEntity(item, seenEntityIds)}`;
`ui.components.logbook.${EVENT_LOCALIZE_MAP[item.context_event_type!]}`
)}`;
}
return `${this.hass.localize(
"ui.components.logbook.from"
)} ${this.hass.localize("ui.components.logbook.event")} ${
item.context_event_type
}`;
} }
private _renderEntity( private _renderEntity(
@ -353,8 +422,7 @@ class HaLogbookRenderer extends LitElement {
private _formatMessageWithPossibleEntity( private _formatMessageWithPossibleEntity(
message: string, message: string,
seenEntities: string[], seenEntities: string[],
possibleEntity?: string, possibleEntity?: string
localizePrefix?: string
) { ) {
// //
// As we are looking at a log(book), we are doing entity_id // As we are looking at a log(book), we are doing entity_id
@ -376,7 +444,7 @@ class HaLogbookRenderer extends LitElement {
seenEntities.push(entityId); seenEntities.push(entityId);
const messageEnd = messageParts.splice(i); const messageEnd = messageParts.splice(i);
messageEnd.shift(); // remove the entity messageEnd.shift(); // remove the entity
return html` ${messageParts.join(" ")} return html`${messageParts.join(" ")}
${this._renderEntity( ${this._renderEntity(
entityId, entityId,
this.hass.states[entityId].attributes.friendly_name this.hass.states[entityId].attributes.friendly_name
@ -404,8 +472,8 @@ class HaLogbookRenderer extends LitElement {
0, 0,
message.length - possibleEntityName.length message.length - possibleEntityName.length
); );
return html` ${localizePrefix ? this.hass.localize(localizePrefix) : ""} return html`${message}
${message} ${this._renderEntity(possibleEntity, possibleEntityName)}`; ${this._renderEntity(possibleEntity, possibleEntityName)}`;
} }
} }
return message; return message;

View File

@ -344,14 +344,17 @@
}, },
"logbook": { "logbook": {
"entries_not_found": "No logbook events found.", "entries_not_found": "No logbook events found.",
"by_user": "by user", "triggered_by": "triggered by",
"by": "by", "triggered_by_automation": "triggered by automation",
"from": "from", "triggered_by_script": "triggered by script",
"for": "for", "triggered_by_service": "triggered by service",
"event": "event", "triggered_by_numeric_state_of": "triggered by numeric state of",
"from_service": "from service", "triggered_by_state_of": "triggered by state of",
"from_automation": "from automation", "triggered_by_event": "triggered by event",
"from_script": "from script", "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%]", "show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"retrieval_error": "Could not load logbook", "retrieval_error": "Could not load logbook",
"messages": { "messages": {