Logbook: Localize Message & Style updates (#6978)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Zack Barett 2020-09-30 03:24:04 -05:00 committed by GitHub
parent 26162815c8
commit e2427c8dce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 567 additions and 290 deletions

View File

@ -7,6 +7,66 @@
/** Icon to use when no icon specified for domain. */ /** Icon to use when no icon specified for domain. */
export const DEFAULT_DOMAIN_ICON = "hass:bookmark"; 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. */ /** Domains that have a state card. */
export const DOMAINS_WITH_CARD = [ export const DOMAINS_WITH_CARD = [
"climate", "climate",
@ -63,6 +123,10 @@ export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];
/** States that we consider "off". */ /** States that we consider "off". */
export const STATES_OFF = ["closed", "locked", "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. */ /** Domains where we allow toggle in Lovelace. */
export const DOMAINS_TOGGLE = new Set([ export const DOMAINS_TOGGLE = new Set([
"fan", "fan",

View File

@ -2,9 +2,9 @@ import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */ /** Return an icon representing a binary sensor state. */
export const binarySensorIcon = (state: HassEntity) => { export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
const is_off = state.state && state.state === "off"; const is_off = state === "off";
switch (state.attributes.device_class) { switch (stateObj?.attributes.device_class) {
case "battery": case "battery":
return is_off ? "hass:battery" : "hass:battery-outline"; return is_off ? "hass:battery" : "hass:battery-outline";
case "battery_charging": case "battery_charging":

View File

@ -9,14 +9,17 @@ import { computeStateDomain } from "./compute_state_domain";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
language: string language: string,
state?: string
): string => { ): string => {
if (stateObj.state === UNKNOWN || stateObj.state === UNAVAILABLE) { const compareState = state !== undefined ? state : stateObj.state;
return localize(`state.default.${stateObj.state}`);
if (compareState === UNKNOWN || compareState === UNAVAILABLE) {
return localize(`state.default.${compareState}`);
} }
if (stateObj.attributes.unit_of_measurement) { 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); const domain = computeStateDomain(stateObj);
@ -56,7 +59,7 @@ export const computeStateDisplay = (
} }
if (domain === "humidifier") { if (domain === "humidifier") {
if (stateObj.state === "on" && stateObj.attributes.humidity) { if (compareState === "on" && stateObj.attributes.humidity) {
return `${stateObj.attributes.humidity}%`; return `${stateObj.attributes.humidity}%`;
} }
} }
@ -65,11 +68,11 @@ export const computeStateDisplay = (
// Return device class translation // Return device class translation
(stateObj.attributes.device_class && (stateObj.attributes.device_class &&
localize( localize(
`component.${domain}.state.${stateObj.attributes.device_class}.${stateObj.state}` `component.${domain}.state.${stateObj.attributes.device_class}.${compareState}`
)) || )) ||
// Return default translation // Return default translation
localize(`component.${domain}.state._.${stateObj.state}`) || localize(`component.${domain}.state._.${compareState}`) ||
// We don't know! Return the raw state. // We don't know! Return the raw state.
stateObj.state compareState
); );
}; };

View File

@ -1,13 +1,12 @@
/** Return an icon representing a cover state. */ /** Return an icon representing a cover state. */
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { domainIcon } from "./domain_icon";
export const coverIcon = (state: HassEntity): string => { export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
const open = state.state !== "closed"; const open = state !== "closed";
switch (state.attributes.device_class) { switch (stateObj?.attributes.device_class) {
case "garage": case "garage":
switch (state.state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return "hass:arrow-up-box";
case "closing": case "closing":
@ -18,7 +17,7 @@ export const coverIcon = (state: HassEntity): string => {
return "hass:garage-open"; return "hass:garage-open";
} }
case "gate": case "gate":
switch (state.state) { switch (state) {
case "opening": case "opening":
case "closing": case "closing":
return "hass:gate-arrow-right"; return "hass:gate-arrow-right";
@ -32,7 +31,7 @@ export const coverIcon = (state: HassEntity): string => {
case "damper": case "damper":
return open ? "hass:circle" : "hass:circle-slice-8"; return open ? "hass:circle" : "hass:circle-slice-8";
case "shutter": case "shutter":
switch (state.state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return "hass:arrow-up-box";
case "closing": case "closing":
@ -44,7 +43,7 @@ export const coverIcon = (state: HassEntity): string => {
} }
case "blind": case "blind":
case "curtain": case "curtain":
switch (state.state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return "hass:arrow-up-box";
case "closing": case "closing":
@ -55,7 +54,7 @@ export const coverIcon = (state: HassEntity): string => {
return "hass:blinds-open"; return "hass:blinds-open";
} }
case "window": case "window":
switch (state.state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return "hass:arrow-up-box";
case "closing": case "closing":
@ -65,7 +64,16 @@ export const coverIcon = (state: HassEntity): string => {
default: default:
return "hass:window-open"; 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: default:
return domainIcon("cover", state.state); return "hass:window-open";
} }
}; };

View File

@ -1,63 +1,24 @@
import { HassEntity } from "home-assistant-js-websocket";
/** /**
* Return the icon to be used for a domain. * Return the icon to be used for a domain.
* *
* Optionally pass in a state to influence the domain icon. * 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 = { export const domainIcon = (
alert: "hass:alert", domain: string,
alexa: "hass:amazon-alexa", stateObj?: HassEntity,
air_quality: "hass:air-filter", state?: string
automation: "hass:robot", ): string => {
calendar: "hass:calendar", const compareState = state !== undefined ? state : stateObj?.state;
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];
}
switch (domain) { switch (domain) {
case "alarm_control_panel": case "alarm_control_panel":
switch (state) { switch (compareState) {
case "armed_home": case "armed_home":
return "hass:bell-plus"; return "hass:bell-plus";
case "armed_night": case "armed_night":
@ -71,21 +32,10 @@ export const domainIcon = (domain: string, state?: string): string => {
} }
case "binary_sensor": case "binary_sensor":
return state && state === "off" return binarySensorIcon(compareState, stateObj);
? "hass:radiobox-blank"
: "hass:checkbox-marked-circle";
case "cover": case "cover":
switch (state) { return coverIcon(compareState, stateObj);
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";
}
case "humidifier": case "humidifier":
return state && state === "off" return state && state === "off"
@ -93,13 +43,13 @@ export const domainIcon = (domain: string, state?: string): string => {
: "hass:air-humidifier"; : "hass:air-humidifier";
case "lock": case "lock":
return state && state === "unlocked" ? "hass:lock-open" : "hass:lock"; return compareState === "unlocked" ? "hass:lock-open" : "hass:lock";
case "media_player": case "media_player":
return state && state === "playing" ? "hass:cast-connected" : "hass:cast"; return compareState === "playing" ? "hass:cast-connected" : "hass:cast";
case "zwave": case "zwave":
switch (state) { switch (compareState) {
case "dead": case "dead":
return "hass:emoticon-dead"; return "hass:emoticon-dead";
case "sleeping": case "sleeping":
@ -110,11 +60,32 @@ export const domainIcon = (domain: string, state?: string): string => {
return "hass:z-wave"; return "hass:z-wave";
} }
default: case "sensor": {
// eslint-disable-next-line const icon = sensorIcon(stateObj);
console.warn( if (icon) {
"Unable to find icon for domain " + domain + " (" + state + ")" return icon;
); }
return DEFAULT_DOMAIN_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;
}; };

View File

@ -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");
};

View File

@ -1,36 +1,23 @@
/** Return an icon representing a sensor state. */ /** Return an icon representing a sensor state. */
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNIT_C, UNIT_F } from "../const"; import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const";
import { domainIcon } from "./domain_icon";
import { batteryIcon } from "./battery_icon"; import { batteryIcon } from "./battery_icon";
const fixedDeviceClassIcons = { export const sensorIcon = (stateObj?: HassEntity): string | undefined => {
current: "hass:current-ac", const dclass = stateObj?.attributes.device_class;
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 = (state: HassEntity) => { if (dclass && dclass in FIXED_DEVICE_CLASS_ICONS) {
const dclass = state.attributes.device_class; return FIXED_DEVICE_CLASS_ICONS[dclass];
if (dclass && dclass in fixedDeviceClassIcons) {
return fixedDeviceClassIcons[dclass];
} }
if (dclass === "battery") { 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) { if (unit === UNIT_C || unit === UNIT_F) {
return "hass:thermometer"; return "hass:thermometer";
} }
return domainIcon("sensor");
return undefined;
}; };

View File

@ -1,19 +1,8 @@
/** Return an icon representing a state. */ /** Return an icon representing a state. */
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { DEFAULT_DOMAIN_ICON } from "../const"; import { DEFAULT_DOMAIN_ICON } from "../const";
import { binarySensorIcon } from "./binary_sensor_icon";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
import { coverIcon } from "./cover_icon";
import { domainIcon } from "./domain_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) => { export const stateIcon = (state: HassEntity) => {
if (!state) { if (!state) {
@ -23,10 +12,5 @@ export const stateIcon = (state: HassEntity) => {
return state.attributes.icon; return state.attributes.icon;
} }
const domain = computeDomain(state.entity_id); return domainIcon(computeDomain(state.entity_id), state);
if (domain in domainIcons) {
return domainIcons[domain](state);
}
return domainIcon(domain, state.state);
}; };

View File

@ -147,7 +147,7 @@ export class HaStateLabelBadge extends LitElement {
return "hass:alert-circle"; return "hass:alert-circle";
} }
// state == 'disarmed' // state == 'disarmed'
return domainIcon(domain, state.state); return domainIcon(domain, state);
case "binary_sensor": case "binary_sensor":
case "device_tracker": case "device_tracker":
case "updater": case "updater":

View File

@ -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 { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export interface LogbookEntry { export interface LogbookEntry {
when: string; when: string;
name: string; name: string;
message: string; message?: string;
entity_id?: string; entity_id?: string;
domain: string; domain: string;
context_user_id?: string; context_user_id?: string;
@ -13,13 +20,43 @@ export interface LogbookEntry {
context_entity_id?: string; context_entity_id?: string;
context_entity_id_name?: string; context_entity_id_name?: string;
context_name?: string; context_name?: string;
state?: string;
} }
const DATA_CACHE: { const DATA_CACHE: {
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> }; [cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
} = {}; } = {};
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, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
@ -43,9 +80,8 @@ export const getLogbookData = (
} }
if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) { if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) {
return DATA_CACHE[cacheKey][ALL_ENTITIES].then((entities) => const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES];
entities.filter((entity) => entity.entity_id === entityId) return entities.filter((entity) => entity.entity_id === entityId);
);
} }
DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer( DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer(
@ -72,6 +108,168 @@ const getLogbookDataFromServer = async (
return hass.callApi<LogbookEntry[]>("GET", url); return hass.callApi<LogbookEntry[]>("GET", url);
}; };
export const clearLogbookCache = (startDate, endDate) => { export const clearLogbookCache = (startDate: string, endDate: string) => {
DATA_CACHE[`${startDate}${endDate}`] = {}; 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
);
};

View File

@ -60,6 +60,7 @@ export class MoreInfoLogbook extends LitElement {
narrow narrow
no-icon no-icon
no-name no-name
relative-time
.hass=${this.hass} .hass=${this.hass}
.entries=${this._logbookEntries} .entries=${this._logbookEntries}
.userIdToName=${this._persons} .userIdToName=${this._persons}

View File

@ -1,6 +1,6 @@
import { import {
css, css,
CSSResult, CSSResultArray,
customElement, customElement,
eventOptions, eventOptions,
html, html,
@ -15,12 +15,14 @@ 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";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { domainIcon } from "../../common/entity/domain_icon"; import { domainIcon } from "../../common/entity/domain_icon";
import { stateIcon } from "../../common/entity/state_icon";
import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl"; import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-icon"; import "../../components/ha-icon";
import "../../components/ha-relative-time";
import { LogbookEntry } from "../../data/logbook"; import { LogbookEntry } from "../../data/logbook";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@customElement("ha-logbook") @customElement("ha-logbook")
@ -46,6 +48,9 @@ class HaLogbook extends LitElement {
@property({ type: Boolean, attribute: "no-name" }) @property({ type: Boolean, attribute: "no-name" })
public noName = false; public noName = false;
@property({ type: Boolean, attribute: "relative-time" })
public relativeTime = false;
// @ts-ignore // @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number; @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 state = item.entity_id ? this.hass.states[item.entity_id] : undefined;
const item_username = const item_username =
item.context_user_id && this.userIdToName[item.context_user_id]; item.context_user_id && this.userIdToName[item.context_user_id];
return html` return html`
<div class="entry-container"> <div class="entry-container">
${index === 0 || ${index === 0 ||
@ -123,58 +129,73 @@ class HaLogbook extends LitElement {
` `
: html``} : html``}
<div class="entry"> <div class="entry ${classMap({ "no-entity": !item.entity_id })}">
<div class="time">
${formatTimeWithSeconds(new Date(item.when), this.hass.language)}
</div>
<div class="icon-message"> <div class="icon-message">
${!this.noIcon ${!this.noIcon
? html` ? html`
<ha-icon <ha-icon
.icon=${state ? stateIcon(state) : domainIcon(item.domain)} .icon=${domainIcon(
item.entity_id
? computeDomain(item.entity_id)
: item.domain,
state,
item.state
)}
></ha-icon> ></ha-icon>
` `
: ""} : ""}
<div class="message"> <div class="message-relative_time">
${!this.noName <div class="message">
? !item.entity_id ${!this.noName
? html`<span class="name">${item.name}</span>` ? html`<a
: html` href="#"
<a @click=${this._entityClicked}
href="#" .entityId=${item.entity_id}
@click=${this._entityClicked} ><span class="name">${item.name}</span></a
.entityId=${item.entity_id} >`
class="name" : ""}
>${item.name}</a ${item.message}
> ${item_username
` ? ` ${this.hass.localize(
: ""} "ui.components.logbook.by"
${item.message} )} ${item_username}`
${item_username : !item.context_event_type
? ` by ${item_username}` ? ""
: !item.context_event_type : item.context_event_type === "call_service"
? "" ? // Service Call
: item.context_event_type === "call_service" ` ${this.hass.localize("ui.components.logbook.by_service")}
? // Service Call
` by service
${item.context_domain}.${item.context_service}` ${item.context_domain}.${item.context_service}`
: item.context_entity_id === item.entity_id : item.context_entity_id === item.entity_id
? // HomeKit or something that self references ? // HomeKit or something that self references
` by ` ${this.hass.localize("ui.components.logbook.by")}
${ ${
item.context_name item.context_name
? item.context_name ? item.context_name
: item.context_event_type : item.context_event_type
}` }`
: // Another entity such as an automation or script : // Another entity such as an automation or script
html` by html` ${this.hass.localize("ui.components.logbook.by")}
<a <a
href="#" href="#"
@click=${this._entityClicked} @click=${this._entityClicked}
.entityId=${item.context_entity_id} .entityId=${item.context_entity_id}
class="name" class="name"
>${item.context_entity_id_name}</a >${item.context_entity_id_name}</a
>`} >`}
</div>
<div class="secondary">
<span
>${formatTimeWithSeconds(
new Date(item.when),
this.hass.language
)}</span
>
-
<ha-relative-time
.hass=${this.hass}
.datetime=${item.when}
></ha-relative-time>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -188,110 +209,131 @@ class HaLogbook extends LitElement {
} }
private _entityClicked(ev: Event) { private _entityClicked(ev: Event) {
const entityId = (ev.currentTarget as any).entityId;
if (!entityId) {
return;
}
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation();
fireEvent(this, "hass-more-info", { fireEvent(this, "hass-more-info", {
entityId: (ev.target as any).entityId, entityId: entityId,
}); });
} }
static get styles(): CSSResult { static get styles(): CSSResultArray {
return css` return [
:host { haStyle,
display: block; css`
height: 100%; :host {
} display: block;
height: 100%;
}
.rtl { .rtl {
direction: ltr; direction: ltr;
} }
.entry-container { .entry-container {
width: 100%; width: 100%;
} }
.entry { .entry {
display: flex; display: flex;
width: 100%; width: 100%;
line-height: 2em; line-height: 2em;
padding: 8px 16px; padding: 8px 16px;
box-sizing: border-box; box-sizing: border-box;
border-top: 1px solid border-top: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
} }
.time { .entry.no-entity,
display: flex; .no-name .entry {
justify-content: center; cursor: default;
flex-direction: column; }
width: 75px;
flex-shrink: 0;
font-size: 12px;
color: var(--secondary-text-color);
}
.date { .entry:hover {
margin: 8px 0; background-color: rgba(var(--rgb-primary-text-color), 0.04);
padding: 0 16px; }
}
.narrow .date { .narrow:not(.no-icon) .time {
padding: 0 8px; margin-left: 32px;
} }
.rtl .date { .message-relative_time {
direction: rtl; display: flex;
} flex-direction: column;
}
.icon-message { .secondary {
display: flex; font-size: 12px;
align-items: center; line-height: 1.7;
} }
.no-entries { .date {
text-align: center; margin: 8px 0;
color: var(--secondary-text-color); padding: 0 16px;
} }
ha-icon { .narrow .date {
margin: 0 8px 0 16px; padding: 0 8px;
flex-shrink: 0; }
color: var(--primary-text-color);
}
.message { .rtl .date {
color: var(--primary-text-color); direction: rtl;
} }
.no-name .message:first-letter { .icon-message {
text-transform: capitalize; display: flex;
} align-items: center;
}
a { .no-entries {
color: var(--primary-color); text-align: center;
} color: var(--secondary-text-color);
}
.uni-virtualizer-host { ha-icon {
display: block; margin-right: 16px;
position: relative; flex-shrink: 0;
contain: strict; color: var(--state-icon-color);
height: 100%; }
overflow: auto;
}
.uni-virtualizer-host > * { .message {
box-sizing: border-box; color: var(--primary-text-color);
} }
.narrow .entry { .no-name .message:first-letter {
flex-direction: column; text-transform: capitalize;
line-height: 1.5; }
padding: 8px 0;
}
.narrow .icon-message ha-icon { a {
margin-left: 0; 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;
}
`,
];
} }
} }

View File

@ -115,18 +115,22 @@ export class HaPanelLogbook extends LitElement {
</div> </div>
${this._isLoading ${this._isLoading
? html`<div class="progress-wrapper"> ? html`
<ha-circular-progress <div class="progress-wrapper">
active <ha-circular-progress
alt=${this.hass.localize("ui.common.loading")} active
></ha-circular-progress> alt=${this.hass.localize("ui.common.loading")}
</div>` ></ha-circular-progress>
: html`<ha-logbook </div>
.hass=${this.hass} `
.entries=${this._entries} : html`
.userIdToName=${this._userIdToName} <ha-logbook
virtualize .hass=${this.hass}
></ha-logbook>`} .entries=${this._entries}
.userIdToName=${this._userIdToName}
virtualize
></ha-logbook>
`}
</ha-app-layout> </ha-app-layout>
`; `;
} }
@ -268,6 +272,7 @@ export class HaPanelLogbook extends LitElement {
), ),
this._fetchUserDone, this._fetchUserDone,
]); ]);
// Fixed in TS 3.9 but upgrade out of scope for this PR. // Fixed in TS 3.9 but upgrade out of scope for this PR.
// @ts-ignore // @ts-ignore
this._entries = entries; this._entries = entries;

View File

@ -285,7 +285,34 @@
}, },
"components": { "components": {
"logbook": { "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": {
"entity-picker": { "entity-picker": {