From e2427c8dcebfdcba2657a2a78c6689a234091977 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 30 Sep 2020 03:24:04 -0500 Subject: [PATCH] Logbook: Localize Message & Style updates (#6978) Co-authored-by: Bram Kragten --- src/common/const.ts | 64 ++++ src/common/entity/binary_sensor_icon.ts | 6 +- src/common/entity/compute_state_display.ts | 19 +- src/common/entity/cover_icon.ts | 28 +- src/common/entity/domain_icon.ts | 117 +++---- src/common/entity/input_dateteime_icon.ts | 13 - src/common/entity/sensor_icon.ts | 33 +- src/common/entity/state_icon.ts | 18 +- src/components/entity/ha-state-label-badge.ts | 2 +- src/data/logbook.ts | 210 ++++++++++++- src/dialogs/more-info/ha-more-info-logbook.ts | 1 + src/panels/logbook/ha-logbook.ts | 288 ++++++++++-------- src/panels/logbook/ha-panel-logbook.ts | 29 +- src/translations/en.json | 29 +- 14 files changed, 567 insertions(+), 290 deletions(-) delete mode 100644 src/common/entity/input_dateteime_icon.ts diff --git a/src/common/const.ts b/src/common/const.ts index 640c4fb13a..4b4f8e6225 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -7,6 +7,66 @@ /** Icon to use when no icon specified for domain. */ export const DEFAULT_DOMAIN_ICON = "hass:bookmark"; +/** Icons for each domain */ +export const FIXED_DOMAIN_ICONS = { + alert: "hass:alert", + alexa: "hass:amazon-alexa", + air_quality: "hass:air-filter", + automation: "hass:robot", + calendar: "hass:calendar", + camera: "hass:video", + climate: "hass:thermostat", + configurator: "hass:cog", + conversation: "hass:text-to-speech", + counter: "hass:counter", + device_tracker: "hass:account", + fan: "hass:fan", + google_assistant: "hass:google-assistant", + group: "hass:google-circles-communities", + homeassistant: "hass:home-assistant", + homekit: "hass:home-automation", + image_processing: "hass:image-filter-frames", + input_boolean: "hass:toggle-switch-outline", + input_datetime: "hass:calendar-clock", + input_number: "hass:ray-vertex", + input_select: "hass:format-list-bulleted", + input_text: "hass:form-textbox", + light: "hass:lightbulb", + mailbox: "hass:mailbox", + notify: "hass:comment-alert", + persistent_notification: "hass:bell", + person: "hass:account", + plant: "hass:flower", + proximity: "hass:apple-safari", + remote: "hass:remote", + scene: "hass:palette", + script: "hass:script-text", + sensor: "hass:eye", + simple_alarm: "hass:bell", + sun: "hass:white-balance-sunny", + switch: "hass:flash", + timer: "hass:timer-outline", + updater: "hass:cloud-upload", + vacuum: "hass:robot-vacuum", + water_heater: "hass:thermometer", + weather: "hass:weather-cloudy", + zone: "hass:map-marker-radius", +}; + +export const FIXED_DEVICE_CLASS_ICONS = { + current: "hass:current-ac", + energy: "hass:flash", + humidity: "hass:water-percent", + illuminance: "hass:brightness-5", + temperature: "hass:thermometer", + pressure: "hass:gauge", + power: "hass:flash", + power_factor: "hass:angle-acute", + signal_strength: "hass:wifi", + timestamp: "hass:clock", + voltage: "hass:sine-wave", +}; + /** Domains that have a state card. */ export const DOMAINS_WITH_CARD = [ "climate", @@ -63,6 +123,10 @@ export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"]; /** States that we consider "off". */ export const STATES_OFF = ["closed", "locked", "off"]; +/** Binary States */ +export const BINARY_STATE_ON = "on"; +export const BINARY_STATE_OFF = "off"; + /** Domains where we allow toggle in Lovelace. */ export const DOMAINS_TOGGLE = new Set([ "fan", diff --git a/src/common/entity/binary_sensor_icon.ts b/src/common/entity/binary_sensor_icon.ts index b2ee9567cd..03c3abaaab 100644 --- a/src/common/entity/binary_sensor_icon.ts +++ b/src/common/entity/binary_sensor_icon.ts @@ -2,9 +2,9 @@ import { HassEntity } from "home-assistant-js-websocket"; /** Return an icon representing a binary sensor state. */ -export const binarySensorIcon = (state: HassEntity) => { - const is_off = state.state && state.state === "off"; - switch (state.attributes.device_class) { +export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => { + const is_off = state === "off"; + switch (stateObj?.attributes.device_class) { case "battery": return is_off ? "hass:battery" : "hass:battery-outline"; case "battery_charging": diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 10de4cb847..2a437901ea 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -9,14 +9,17 @@ import { computeStateDomain } from "./compute_state_domain"; export const computeStateDisplay = ( localize: LocalizeFunc, stateObj: HassEntity, - language: string + language: string, + state?: string ): string => { - if (stateObj.state === UNKNOWN || stateObj.state === UNAVAILABLE) { - return localize(`state.default.${stateObj.state}`); + const compareState = state !== undefined ? state : stateObj.state; + + if (compareState === UNKNOWN || compareState === UNAVAILABLE) { + return localize(`state.default.${compareState}`); } if (stateObj.attributes.unit_of_measurement) { - return `${stateObj.state} ${stateObj.attributes.unit_of_measurement}`; + return `${compareState} ${stateObj.attributes.unit_of_measurement}`; } const domain = computeStateDomain(stateObj); @@ -56,7 +59,7 @@ export const computeStateDisplay = ( } if (domain === "humidifier") { - if (stateObj.state === "on" && stateObj.attributes.humidity) { + if (compareState === "on" && stateObj.attributes.humidity) { return `${stateObj.attributes.humidity}%`; } } @@ -65,11 +68,11 @@ export const computeStateDisplay = ( // Return device class translation (stateObj.attributes.device_class && localize( - `component.${domain}.state.${stateObj.attributes.device_class}.${stateObj.state}` + `component.${domain}.state.${stateObj.attributes.device_class}.${compareState}` )) || // Return default translation - localize(`component.${domain}.state._.${stateObj.state}`) || + localize(`component.${domain}.state._.${compareState}`) || // We don't know! Return the raw state. - stateObj.state + compareState ); }; diff --git a/src/common/entity/cover_icon.ts b/src/common/entity/cover_icon.ts index 25d3c265a9..5723ee2d34 100644 --- a/src/common/entity/cover_icon.ts +++ b/src/common/entity/cover_icon.ts @@ -1,13 +1,12 @@ /** Return an icon representing a cover state. */ import { HassEntity } from "home-assistant-js-websocket"; -import { domainIcon } from "./domain_icon"; -export const coverIcon = (state: HassEntity): string => { - const open = state.state !== "closed"; +export const coverIcon = (state?: string, stateObj?: HassEntity): string => { + const open = state !== "closed"; - switch (state.attributes.device_class) { + switch (stateObj?.attributes.device_class) { case "garage": - switch (state.state) { + switch (state) { case "opening": return "hass:arrow-up-box"; case "closing": @@ -18,7 +17,7 @@ export const coverIcon = (state: HassEntity): string => { return "hass:garage-open"; } case "gate": - switch (state.state) { + switch (state) { case "opening": case "closing": return "hass:gate-arrow-right"; @@ -32,7 +31,7 @@ export const coverIcon = (state: HassEntity): string => { case "damper": return open ? "hass:circle" : "hass:circle-slice-8"; case "shutter": - switch (state.state) { + switch (state) { case "opening": return "hass:arrow-up-box"; case "closing": @@ -44,7 +43,7 @@ export const coverIcon = (state: HassEntity): string => { } case "blind": case "curtain": - switch (state.state) { + switch (state) { case "opening": return "hass:arrow-up-box"; case "closing": @@ -55,7 +54,7 @@ export const coverIcon = (state: HassEntity): string => { return "hass:blinds-open"; } case "window": - switch (state.state) { + switch (state) { case "opening": return "hass:arrow-up-box"; case "closing": @@ -65,7 +64,16 @@ export const coverIcon = (state: HassEntity): string => { default: return "hass:window-open"; } + } + + switch (state) { + case "opening": + return "hass:arrow-up-box"; + case "closing": + return "hass:arrow-down-box"; + case "closed": + return "hass:window-closed"; default: - return domainIcon("cover", state.state); + return "hass:window-open"; } }; diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index 4304d88353..e555b0e286 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -1,63 +1,24 @@ +import { HassEntity } from "home-assistant-js-websocket"; /** * Return the icon to be used for a domain. * * Optionally pass in a state to influence the domain icon. */ -import { DEFAULT_DOMAIN_ICON } from "../const"; +import { DEFAULT_DOMAIN_ICON, FIXED_DOMAIN_ICONS } from "../const"; +import { binarySensorIcon } from "./binary_sensor_icon"; +import { coverIcon } from "./cover_icon"; +import { sensorIcon } from "./sensor_icon"; -const fixedIcons = { - alert: "hass:alert", - alexa: "hass:amazon-alexa", - air_quality: "hass:air-filter", - automation: "hass:robot", - calendar: "hass:calendar", - camera: "hass:video", - climate: "hass:thermostat", - configurator: "hass:cog", - conversation: "hass:text-to-speech", - counter: "hass:counter", - device_tracker: "hass:account", - fan: "hass:fan", - google_assistant: "hass:google-assistant", - group: "hass:google-circles-communities", - homeassistant: "hass:home-assistant", - homekit: "hass:home-automation", - image_processing: "hass:image-filter-frames", - input_boolean: "hass:toggle-switch-outline", - input_datetime: "hass:calendar-clock", - input_number: "hass:ray-vertex", - input_select: "hass:format-list-bulleted", - input_text: "hass:form-textbox", - light: "hass:lightbulb", - mailbox: "hass:mailbox", - notify: "hass:comment-alert", - persistent_notification: "hass:bell", - person: "hass:account", - plant: "hass:flower", - proximity: "hass:apple-safari", - remote: "hass:remote", - scene: "hass:palette", - script: "hass:script-text", - sensor: "hass:eye", - simple_alarm: "hass:bell", - sun: "hass:white-balance-sunny", - switch: "hass:flash", - timer: "hass:timer-outline", - updater: "hass:cloud-upload", - vacuum: "hass:robot-vacuum", - water_heater: "hass:thermometer", - weather: "hass:weather-cloudy", - zone: "hass:map-marker-radius", -}; - -export const domainIcon = (domain: string, state?: string): string => { - if (domain in fixedIcons) { - return fixedIcons[domain]; - } +export const domainIcon = ( + domain: string, + stateObj?: HassEntity, + state?: string +): string => { + const compareState = state !== undefined ? state : stateObj?.state; switch (domain) { case "alarm_control_panel": - switch (state) { + switch (compareState) { case "armed_home": return "hass:bell-plus"; case "armed_night": @@ -71,21 +32,10 @@ export const domainIcon = (domain: string, state?: string): string => { } case "binary_sensor": - return state && state === "off" - ? "hass:radiobox-blank" - : "hass:checkbox-marked-circle"; + return binarySensorIcon(compareState, stateObj); case "cover": - switch (state) { - case "opening": - return "hass:arrow-up-box"; - case "closing": - return "hass:arrow-down-box"; - case "closed": - return "hass:window-closed"; - default: - return "hass:window-open"; - } + return coverIcon(compareState, stateObj); case "humidifier": return state && state === "off" @@ -93,13 +43,13 @@ export const domainIcon = (domain: string, state?: string): string => { : "hass:air-humidifier"; case "lock": - return state && state === "unlocked" ? "hass:lock-open" : "hass:lock"; + return compareState === "unlocked" ? "hass:lock-open" : "hass:lock"; case "media_player": - return state && state === "playing" ? "hass:cast-connected" : "hass:cast"; + return compareState === "playing" ? "hass:cast-connected" : "hass:cast"; case "zwave": - switch (state) { + switch (compareState) { case "dead": return "hass:emoticon-dead"; case "sleeping": @@ -110,11 +60,32 @@ export const domainIcon = (domain: string, state?: string): string => { return "hass:z-wave"; } - default: - // eslint-disable-next-line - console.warn( - "Unable to find icon for domain " + domain + " (" + state + ")" - ); - return DEFAULT_DOMAIN_ICON; + case "sensor": { + const icon = sensorIcon(stateObj); + if (icon) { + return icon; + } + + break; + } + + case "input_datetime": + if (!stateObj?.attributes.has_date) { + return "hass:clock"; + } + if (!stateObj.attributes.has_time) { + return "hass:calendar"; + } + break; } + + if (domain in FIXED_DOMAIN_ICONS) { + return FIXED_DOMAIN_ICONS[domain]; + } + + // eslint-disable-next-line + console.warn( + "Unable to find icon for domain " + domain + " (" + stateObj + ")" + ); + return DEFAULT_DOMAIN_ICON; }; diff --git a/src/common/entity/input_dateteime_icon.ts b/src/common/entity/input_dateteime_icon.ts deleted file mode 100644 index 75644ad72b..0000000000 --- a/src/common/entity/input_dateteime_icon.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** Return an icon representing an input datetime state. */ -import { HassEntity } from "home-assistant-js-websocket"; -import { domainIcon } from "./domain_icon"; - -export const inputDateTimeIcon = (state: HassEntity): string => { - if (!state.attributes.has_date) { - return "hass:clock"; - } - if (!state.attributes.has_time) { - return "hass:calendar"; - } - return domainIcon("input_datetime"); -}; diff --git a/src/common/entity/sensor_icon.ts b/src/common/entity/sensor_icon.ts index 1447b41e31..16d5b5c065 100644 --- a/src/common/entity/sensor_icon.ts +++ b/src/common/entity/sensor_icon.ts @@ -1,36 +1,23 @@ /** Return an icon representing a sensor state. */ import { HassEntity } from "home-assistant-js-websocket"; -import { UNIT_C, UNIT_F } from "../const"; -import { domainIcon } from "./domain_icon"; +import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const"; import { batteryIcon } from "./battery_icon"; -const fixedDeviceClassIcons = { - current: "hass:current-ac", - energy: "hass:flash", - humidity: "hass:water-percent", - illuminance: "hass:brightness-5", - temperature: "hass:thermometer", - pressure: "hass:gauge", - power: "hass:flash", - power_factor: "hass:angle-acute", - signal_strength: "hass:wifi", - timestamp: "hass:clock", - voltage: "hass:sine-wave", -}; +export const sensorIcon = (stateObj?: HassEntity): string | undefined => { + const dclass = stateObj?.attributes.device_class; -export const sensorIcon = (state: HassEntity) => { - const dclass = state.attributes.device_class; - - if (dclass && dclass in fixedDeviceClassIcons) { - return fixedDeviceClassIcons[dclass]; + if (dclass && dclass in FIXED_DEVICE_CLASS_ICONS) { + return FIXED_DEVICE_CLASS_ICONS[dclass]; } + if (dclass === "battery") { - return batteryIcon(state); + return stateObj ? batteryIcon(stateObj) : "hass:battery"; } - const unit = state.attributes.unit_of_measurement; + const unit = stateObj?.attributes.unit_of_measurement; if (unit === UNIT_C || unit === UNIT_F) { return "hass:thermometer"; } - return domainIcon("sensor"); + + return undefined; }; diff --git a/src/common/entity/state_icon.ts b/src/common/entity/state_icon.ts index 7291f6863e..c87c7bdd2f 100644 --- a/src/common/entity/state_icon.ts +++ b/src/common/entity/state_icon.ts @@ -1,19 +1,8 @@ /** Return an icon representing a state. */ import { HassEntity } from "home-assistant-js-websocket"; import { DEFAULT_DOMAIN_ICON } from "../const"; -import { binarySensorIcon } from "./binary_sensor_icon"; import { computeDomain } from "./compute_domain"; -import { coverIcon } from "./cover_icon"; import { domainIcon } from "./domain_icon"; -import { inputDateTimeIcon } from "./input_dateteime_icon"; -import { sensorIcon } from "./sensor_icon"; - -const domainIcons = { - binary_sensor: binarySensorIcon, - cover: coverIcon, - sensor: sensorIcon, - input_datetime: inputDateTimeIcon, -}; export const stateIcon = (state: HassEntity) => { if (!state) { @@ -23,10 +12,5 @@ export const stateIcon = (state: HassEntity) => { return state.attributes.icon; } - const domain = computeDomain(state.entity_id); - - if (domain in domainIcons) { - return domainIcons[domain](state); - } - return domainIcon(domain, state.state); + return domainIcon(computeDomain(state.entity_id), state); }; diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 3441979436..675a7b739a 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -147,7 +147,7 @@ export class HaStateLabelBadge extends LitElement { return "hass:alert-circle"; } // state == 'disarmed' - return domainIcon(domain, state.state); + return domainIcon(domain, state); case "binary_sensor": case "device_tracker": case "updater": diff --git a/src/data/logbook.ts b/src/data/logbook.ts index 00fd8e937d..cb93ee4003 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -1,9 +1,16 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const"; +import { computeDomain } from "../common/entity/compute_domain"; +import { computeStateDisplay } from "../common/entity/compute_state_display"; import { HomeAssistant } from "../types"; +import { UNAVAILABLE_STATES } from "./entity"; + +const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages"; export interface LogbookEntry { when: string; name: string; - message: string; + message?: string; entity_id?: string; domain: string; context_user_id?: string; @@ -13,13 +20,43 @@ export interface LogbookEntry { context_entity_id?: string; context_entity_id_name?: string; context_name?: string; + state?: string; } const DATA_CACHE: { [cacheKey: string]: { [entityId: string]: Promise }; } = {}; -export const getLogbookData = ( +export const getLogbookData = async ( + hass: HomeAssistant, + startDate: string, + endDate: string, + entityId?: string, + entity_matches_only?: boolean +) => { + const logbookData = await getLogbookDataCache( + hass, + startDate, + endDate, + entityId, + entity_matches_only + ); + + for (const entry of logbookData) { + if (entry.state) { + entry.message = getLogbookMessage( + hass, + entry.state, + hass!.states[entry.entity_id!], + computeDomain(entry.entity_id!) + ); + } + } + + return logbookData; +}; + +export const getLogbookDataCache = async ( hass: HomeAssistant, startDate: string, endDate: string, @@ -43,9 +80,8 @@ export const getLogbookData = ( } if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) { - return DATA_CACHE[cacheKey][ALL_ENTITIES].then((entities) => - entities.filter((entity) => entity.entity_id === entityId) - ); + const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES]; + return entities.filter((entity) => entity.entity_id === entityId); } DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer( @@ -72,6 +108,168 @@ const getLogbookDataFromServer = async ( return hass.callApi("GET", url); }; -export const clearLogbookCache = (startDate, endDate) => { +export const clearLogbookCache = (startDate: string, endDate: string) => { DATA_CACHE[`${startDate}${endDate}`] = {}; }; + +export const getLogbookMessage = ( + hass: HomeAssistant, + state: string, + stateObj: HassEntity, + domain: string +): string => { + switch (domain) { + case "device_tracker": + case "person": + return state === "not_home" + ? hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`) + : hass.localize( + `${LOGBOOK_LOCALIZE_PATH}.was_at_state`, + "state", + state + ); + + case "sun": + return state === "above_horizon" + ? hass.localize(`${LOGBOOK_LOCALIZE_PATH}.rose`) + : hass.localize(`${LOGBOOK_LOCALIZE_PATH}.set`); + + case "binary_sensor": { + const isOn = state === BINARY_STATE_ON; + const isOff = state === BINARY_STATE_OFF; + const device_class = stateObj.attributes.device_class; + + switch (device_class) { + case "battery": + if (isOn) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_low`); + } + if (isOff) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_normal`); + } + break; + + case "connectivity": + if (isOn) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_connected`); + } + if (isOff) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_disconnected`); + } + break; + + case "door": + case "garage_door": + case "opening": + case "window": + if (isOn) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`); + } + if (isOff) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`); + } + break; + + case "lock": + if (isOn) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`); + } + if (isOff) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`); + } + break; + + case "plug": + if (isOn) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_plugged_in`); + } + if (isOff) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unplugged`); + } + break; + + case "presence": + if (isOn) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`); + } + if (isOff) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`); + } + break; + + case "safety": + if (isOn) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unsafe`); + } + if (isOff) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_safe`); + } + break; + + case "cold": + case "gas": + case "heat": + case "colightld": + case "moisture": + case "motion": + case "occupancy": + case "power": + case "problem": + case "smoke": + case "sound": + case "vibration": + if (isOn) { + return hass.localize( + `${LOGBOOK_LOCALIZE_PATH}.detected_device_class`, + "device_class", + device_class + ); + } + if (isOff) { + return hass.localize( + `${LOGBOOK_LOCALIZE_PATH}.cleared_device_class`, + "device_class", + device_class + ); + } + break; + } + + break; + } + + case "cover": + return state === "open" + ? hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`) + : hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`); + + case "lock": + if (state === "unlocked") { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`); + } + if (state === "locked") { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`); + } + break; + } + + if (state === BINARY_STATE_ON) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.turned_on`); + } + + if (state === BINARY_STATE_OFF) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.turned_off`); + } + + if (UNAVAILABLE_STATES.includes(state)) { + return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.became_unavailable`); + } + + return hass.localize( + `${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, + "state", + stateObj + ? computeStateDisplay(hass.localize, stateObj, hass.language, state) + : state + ); +}; diff --git a/src/dialogs/more-info/ha-more-info-logbook.ts b/src/dialogs/more-info/ha-more-info-logbook.ts index ecb3a7d85e..76d05562c5 100644 --- a/src/dialogs/more-info/ha-more-info-logbook.ts +++ b/src/dialogs/more-info/ha-more-info-logbook.ts @@ -60,6 +60,7 @@ export class MoreInfoLogbook extends LitElement { narrow no-icon no-name + relative-time .hass=${this.hass} .entries=${this._logbookEntries} .userIdToName=${this._persons} diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 9c5e85d5df..3d86c88b3d 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -1,6 +1,6 @@ import { css, - CSSResult, + CSSResultArray, customElement, eventOptions, html, @@ -15,12 +15,14 @@ import { formatDate } from "../../common/datetime/format_date"; import { formatTimeWithSeconds } from "../../common/datetime/format_time"; import { restoreScroll } from "../../common/decorators/restore-scroll"; import { fireEvent } from "../../common/dom/fire_event"; +import { computeDomain } from "../../common/entity/compute_domain"; import { domainIcon } from "../../common/entity/domain_icon"; -import { stateIcon } from "../../common/entity/state_icon"; import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl"; import "../../components/ha-circular-progress"; import "../../components/ha-icon"; +import "../../components/ha-relative-time"; import { LogbookEntry } from "../../data/logbook"; +import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; @customElement("ha-logbook") @@ -46,6 +48,9 @@ class HaLogbook extends LitElement { @property({ type: Boolean, attribute: "no-name" }) public noName = false; + @property({ type: Boolean, attribute: "relative-time" }) + public relativeTime = false; + // @ts-ignore @restoreScroll(".container") private _savedScrollPos?: number; @@ -109,6 +114,7 @@ class HaLogbook extends LitElement { const state = item.entity_id ? this.hass.states[item.entity_id] : undefined; const item_username = item.context_user_id && this.userIdToName[item.context_user_id]; + return html`
${index === 0 || @@ -123,58 +129,73 @@ class HaLogbook extends LitElement { ` : html``} -
-
- ${formatTimeWithSeconds(new Date(item.when), this.hass.language)} -
+
${!this.noIcon ? html` ` : ""} -
- ${!this.noName - ? !item.entity_id - ? html`${item.name}` - : html` - ${item.name} - ` - : ""} - ${item.message} - ${item_username - ? ` by ${item_username}` - : !item.context_event_type - ? "" - : item.context_event_type === "call_service" - ? // Service Call - ` by service +
+
+ ${!this.noName + ? html`${item.name}` + : ""} + ${item.message} + ${item_username + ? ` ${this.hass.localize( + "ui.components.logbook.by" + )} ${item_username}` + : !item.context_event_type + ? "" + : item.context_event_type === "call_service" + ? // Service Call + ` ${this.hass.localize("ui.components.logbook.by_service")} ${item.context_domain}.${item.context_service}` - : item.context_entity_id === item.entity_id - ? // HomeKit or something that self references - ` by + : item.context_entity_id === item.entity_id + ? // HomeKit or something that self references + ` ${this.hass.localize("ui.components.logbook.by")} ${ item.context_name ? item.context_name : item.context_event_type }` - : // Another entity such as an automation or script - html` by - ${item.context_entity_id_name}`} + : // Another entity such as an automation or script + html` ${this.hass.localize("ui.components.logbook.by")} + ${item.context_entity_id_name}`} +
+
+ ${formatTimeWithSeconds( + new Date(item.when), + this.hass.language + )} + - + +
@@ -188,110 +209,131 @@ class HaLogbook extends LitElement { } private _entityClicked(ev: Event) { + const entityId = (ev.currentTarget as any).entityId; + if (!entityId) { + return; + } + ev.preventDefault(); + ev.stopPropagation(); fireEvent(this, "hass-more-info", { - entityId: (ev.target as any).entityId, + entityId: entityId, }); } - static get styles(): CSSResult { - return css` - :host { - display: block; - height: 100%; - } + static get styles(): CSSResultArray { + return [ + haStyle, + css` + :host { + display: block; + height: 100%; + } - .rtl { - direction: ltr; - } + .rtl { + direction: ltr; + } - .entry-container { - width: 100%; - } + .entry-container { + width: 100%; + } - .entry { - display: flex; - width: 100%; - line-height: 2em; - padding: 8px 16px; - box-sizing: border-box; - border-top: 1px solid - var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); - } + .entry { + display: flex; + width: 100%; + line-height: 2em; + padding: 8px 16px; + box-sizing: border-box; + border-top: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + } - .time { - display: flex; - justify-content: center; - flex-direction: column; - width: 75px; - flex-shrink: 0; - font-size: 12px; - color: var(--secondary-text-color); - } + .entry.no-entity, + .no-name .entry { + cursor: default; + } - .date { - margin: 8px 0; - padding: 0 16px; - } + .entry:hover { + background-color: rgba(var(--rgb-primary-text-color), 0.04); + } - .narrow .date { - padding: 0 8px; - } + .narrow:not(.no-icon) .time { + margin-left: 32px; + } - .rtl .date { - direction: rtl; - } + .message-relative_time { + display: flex; + flex-direction: column; + } - .icon-message { - display: flex; - align-items: center; - } + .secondary { + font-size: 12px; + line-height: 1.7; + } - .no-entries { - text-align: center; - color: var(--secondary-text-color); - } + .date { + margin: 8px 0; + padding: 0 16px; + } - ha-icon { - margin: 0 8px 0 16px; - flex-shrink: 0; - color: var(--primary-text-color); - } + .narrow .date { + padding: 0 8px; + } - .message { - color: var(--primary-text-color); - } + .rtl .date { + direction: rtl; + } - .no-name .message:first-letter { - text-transform: capitalize; - } + .icon-message { + display: flex; + align-items: center; + } - a { - color: var(--primary-color); - } + .no-entries { + text-align: center; + color: var(--secondary-text-color); + } - .uni-virtualizer-host { - display: block; - position: relative; - contain: strict; - height: 100%; - overflow: auto; - } + ha-icon { + margin-right: 16px; + flex-shrink: 0; + color: var(--state-icon-color); + } - .uni-virtualizer-host > * { - box-sizing: border-box; - } + .message { + color: var(--primary-text-color); + } - .narrow .entry { - flex-direction: column; - line-height: 1.5; - padding: 8px 0; - } + .no-name .message:first-letter { + text-transform: capitalize; + } - .narrow .icon-message ha-icon { - margin-left: 0; - } - `; + a { + color: var(--primary-color); + } + + .uni-virtualizer-host { + display: block; + position: relative; + contain: strict; + height: 100%; + overflow: auto; + } + + .uni-virtualizer-host > * { + box-sizing: border-box; + } + + .narrow .entry { + line-height: 1.5; + padding: 8px; + } + + .narrow .icon-message ha-icon { + margin-left: 0; + } + `, + ]; } } diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts index 806cb5ac6b..e3542ba572 100644 --- a/src/panels/logbook/ha-panel-logbook.ts +++ b/src/panels/logbook/ha-panel-logbook.ts @@ -115,18 +115,22 @@ export class HaPanelLogbook extends LitElement {
${this._isLoading - ? html`
- -
` - : html``} + ? html` +
+ +
+ ` + : html` + + `} `; } @@ -268,6 +272,7 @@ export class HaPanelLogbook extends LitElement { ), this._fetchUserDone, ]); + // Fixed in TS 3.9 but upgrade out of scope for this PR. // @ts-ignore this._entries = entries; diff --git a/src/translations/en.json b/src/translations/en.json index 172ff5d918..d23e6b68d3 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -285,7 +285,34 @@ }, "components": { "logbook": { - "entries_not_found": "No logbook entries found." + "entries_not_found": "No logbook entries found.", + "by": "by", + "by_service": "by service", + "messages": { + "was_away": "was away", + "was_at_state": "was at {state}", + "rose": "rose", + "set": "set", + "was_low": "was low", + "was_normal": "was normal", + "was_connected": "was connected", + "was_disconnected": "was disconnected", + "was_opened": "was opened", + "was_closed": "was closed", + "was_unlocked": "was unlocked", + "was_locked": "was locked", + "was_plugged_in": "was plugged in", + "was_unplugged": "was unplugged", + "was_at_home": "was at home", + "was_unsafe": "was unsafe", + "was_safe": "was safe", + "detected_device_class": "detected {device_class}", + "cleared_device_class": "cleared (no {device_class} detected)", + "turned_off": "turned off", + "turned_on": "turned on", + "changed_to_state": "changed to {state}", + "became_unavailable": "became unavailable" + } }, "entity": { "entity-picker": {