mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-25 05:47:20 +00:00
Use new localized context state and source in logbook (#12742)
This commit is contained in:
parent
7db6e0b779
commit
a02b817d7f
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user