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 {
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<LogbookEntry[]> };
} = {};
@ -39,17 +60,13 @@ export const getLogbookDataForContext = async (
startDate: string,
contextId?: string
): Promise<LogbookEntry[]> => {
const localize = await hass.loadBackendTranslation("device_class");
return addLogbookMessage(
hass,
localize,
await getLogbookDataFromServer(
await hass.loadBackendTranslation("device_class");
return getLogbookDataFromServer(
hass,
startDate,
undefined,
undefined,
contextId
)
);
};
@ -60,13 +77,9 @@ export const getLogbookData = async (
entityIds?: string[],
deviceIds?: string[]
): Promise<LogbookEntry[]> => {
const localize = await hass.loadBackendTranslation("device_class");
return addLogbookMessage(
hass,
localize,
// bypass cache if we have a device ID
deviceIds?.length
? await getLogbookDataFromServer(
await hass.loadBackendTranslation("device_class");
return deviceIds?.length
? getLogbookDataFromServer(
hass,
startDate,
endDate,
@ -74,28 +87,7 @@ export const getLogbookData = async (
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(
hass,
localize,
entry.state,
stateObj,
computeDomain(entry.entity_id!)
);
}
}
return logbookData;
: 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 => <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,
localize: LocalizeFunc,
state: string,

View File

@ -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 ? <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 =
!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,
${this._renderMessage(
item,
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
)}`
: ""}
domain,
historicStateObj
)}
${this._renderContextMessage(item, seenEntityIds)}
</div>
<div class="secondary">
<span
@ -257,7 +214,8 @@ class HaLogbookRenderer extends LitElement {
.datetime=${item.when * 1000}
capitalize
></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
? html`
-
@ -294,38 +252,149 @@ class HaLogbookRenderer extends LitElement {
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") {
return `${this.hass.localize("ui.components.logbook.from_service")} ${
item.context_domain
}.${item.context_service}`;
return html`${this.hass.localize(
"ui.components.logbook.triggered_by_service"
)}
${item.context_domain}.${item.context_service}`;
}
if (item.context_event_type === "automation_triggered") {
if (seenEntities.includes(item.context_entity_id!)) {
if (
!item.context_message ||
seenEntityIds.includes(item.context_entity_id!)
) {
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)}`;
// 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
)
: ""}`;
}
if (item.context_name) {
return `${this.hass.localize("ui.components.logbook.from")} ${
item.context_name
}`;
}
if (item.context_event_type === "state_changed") {
return "";
}
if (item.context_event_type! in EVENT_LOCALIZE_MAP) {
return `${this.hass.localize(
`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
}`;
// 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;

View File

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