Compare commits

...

6 Commits

Author SHA1 Message Date
Aidan Timson 5ad54953ef Registries, api context 2026-05-12 16:33:00 +01:00
Aidan Timson c82f8c6090 Format 2026-05-12 16:11:44 +01:00
Aidan Timson 0e3f712a3b Add filters for devices and areas 2026-05-12 16:10:58 +01:00
Aidan Timson 7c12ee3cae Format 2026-05-12 16:05:39 +01:00
Aidan Timson e0feb3a7f1 Add device and area to states panel. Use lazy context 2026-05-12 16:03:05 +01:00
George Caliment 60e95b886c Fixed how ha-entity-toggle sets ha-switch styles var (#51984) 2026-05-12 16:46:01 +02:00
3 changed files with 298 additions and 130 deletions
+11 -3
View File
@@ -22,6 +22,14 @@ const isOn = (stateObj?: HassEntity) =>
!STATES_OFF.includes(stateObj.state) &&
!isUnavailableState(stateObj.state);
/**
* @element ha-entity-toggle
*
* @cssprop --ha-entity-toggle-switch-width - Width of the switch track. Defaults to `38px`.
* @cssprop --ha-entity-toggle-switch-size - Height of the switch track. Defaults to `20px`.
* @cssprop --ha-entity-toggle-switch-thumb-size - Size of the switch thumb. Defaults to `14px`.
*/
@customElement("ha-entity-toggle")
export class HaEntityToggle extends LitElement {
// hass is not a property so that we only re-render on stateObj changes
@@ -165,9 +173,9 @@ export class HaEntityToggle extends LitElement {
white-space: nowrap;
}
ha-switch {
--ha-switch-width: 38px;
--ha-switch-size: 20px;
--ha-switch-thumb-size: 14px;
--ha-switch-width: var(--ha-entity-toggle-switch-width, 38px);
--ha-switch-size: var(--ha-entity-toggle-switch-size, 20px);
--ha-switch-thumb-size: var(--ha-entity-toggle-switch-thumb-size, 14px);
}
ha-icon-button {
--ha-icon-button-size: 40px;
@@ -3,24 +3,29 @@ import {
mdiClipboardTextMultipleOutline,
mdiInformationOutline,
} from "@mdi/js";
import { consume, type ContextType } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { dump } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeAreaName } from "../../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { computeEntityEntryName } from "../../../../common/entity/compute_entity_name";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-svg-icon";
import {
internationalizationContext,
registriesContext,
} from "../../../../data/context";
import { haStyle } from "../../../../resources/styles";
import { loadVirtualizer } from "../../../../resources/virtualizer";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
@customElement("developer-tools-state-renderer")
class HaPanelDevStateRenderer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entities: HassEntity[] = [];
@property({ type: Boolean, attribute: "narrow" })
@@ -32,7 +37,15 @@ class HaPanelDevStateRenderer extends LitElement {
@property({ attribute: false })
public showAttributes = true;
protected willUpdate(changedProps: PropertyValues<this>) {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries!: ContextType<typeof registriesContext>;
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (
(!this.hasUpdated && this.virtualize) ||
@@ -42,87 +55,90 @@ class HaPanelDevStateRenderer extends LitElement {
}
}
protected shouldUpdate(changedProps: PropertyValues<this>) {
super.shouldUpdate(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const languageChanged =
oldHass === undefined || oldHass.locale !== this.hass.locale;
return (
changedProps.has("entities") ||
changedProps.has("narrow") ||
changedProps.has("virtualize") ||
changedProps.has("showAttributes") ||
languageChanged
);
}
protected render() {
const showAttributes = !this.narrow && this.showAttributes;
return html`
<div
class=${classMap({ entities: true, "hide-attributes": !showAttributes })}
role="table"
>
<div class="row" role="row" aria-rowindex="1">
<div class="header" role="columnheader">
<span class="padded">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.states.entity"
)}
</span>
</div>
<div class="header" role="columnheader">
<span class="padded">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.states.state"
)}
</span>
</div>
<div class="header" role="columnheader">
<span class="padded">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.states.attributes"
)}
</span>
</div>
<div
class=${classMap({
entities: true,
"hide-attributes": !showAttributes,
"hide-extra": this.narrow,
})}
role="table"
>
<div class="row" role="row" aria-rowindex="1">
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.entity"
)}
</span>
</div>
<div class="row filters" role="row" aria-rowindex="2">
<div class="header filter-entities" role="columnheader">
<slot name="filter-entities"></slot>
</div></span>
<div class="header filter-states" role="columnheader">
<slot name="filter-states"></slot>
</div>
<div class="header filter-attributes" role="columnheader">
<slot name="filter-attributes"></slot>
</div>
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.state"
)}
</span>
</div>
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.entities.picker.headers.device"
)}
</span>
</div>
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize("ui.panel.config.generic.headers.area")}
</span>
</div>
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.attributes"
)}
</span>
</div>
${
this.entities.length === 0
? html` <div class="row" role="row" aria-rowindex="3">
<div class="cell" role="cell" aria-colspan="3">
<span class="padded">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.states.no_entities"
)}
</span>
</div>
</div>`
: nothing
}
${
this.virtualize
? html`<lit-virtualizer
.items=${this.entities}
.renderItem=${this._renderStateItem}
>
</lit-virtualizer>`
: this.entities.map((item, index) =>
this._renderStateItem(item, index)
)
}
</div>
<div class="row filters" role="row" aria-rowindex="2">
<div class="header filter-entities" role="columnheader">
<slot name="filter-entities"></slot>
</div>
<div class="header filter-states" role="columnheader">
<slot name="filter-states"></slot>
</div>
<div class="header filter-devices" role="columnheader">
<slot name="filter-devices"></slot>
</div>
<div class="header filter-areas" role="columnheader">
<slot name="filter-areas"></slot>
</div>
<div class="header filter-attributes" role="columnheader">
<slot name="filter-attributes"></slot>
</div>
</div>
${this.entities.length === 0
? html` <div class="row" role="row" aria-rowindex="3">
<div class="cell" role="cell" aria-colspan="5">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.no_entities"
)}
</span>
</div>
</div>`
: nothing}
${this.virtualize
? html`<lit-virtualizer
.items=${this.entities}
.renderItem=${this._renderStateItem}
>
</lit-virtualizer>`
: this.entities.map((item, index) =>
this._renderStateItem(item, index)
)}
</div>
`;
}
@@ -133,6 +149,20 @@ class HaPanelDevStateRenderer extends LitElement {
if (!item || index === undefined) {
return nothing;
}
const entry = this._registries?.entities?.[item.entity_id];
const device = entry?.device_id
? this._registries?.devices?.[entry.device_id]
: undefined;
const areaId = entry?.area_id || device?.area_id;
const area = areaId ? this._registries?.areas?.[areaId] : undefined;
const displayName = entry
? computeEntityEntryName(entry, this._registries.devices, item)
: undefined;
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
return html`
<div
class=${classMap({
@@ -150,10 +180,10 @@ class HaPanelDevStateRenderer extends LitElement {
<ha-svg-icon
@click=${this._copyEntity}
.entity=${item}
alt=${this.hass.localize(
alt=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.copy_id"
)}
title=${this.hass.localize(
title=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.copy_id"
)}
.path=${mdiClipboardTextMultipleOutline}
@@ -166,16 +196,16 @@ class HaPanelDevStateRenderer extends LitElement {
<ha-svg-icon
@click=${this._entityMoreInfo}
.entity=${item}
alt=${this.hass.localize(
alt=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.more_info"
)}
title=${this.hass.localize(
title=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.more_info"
)}
.path=${mdiInformationOutline}
></ha-svg-icon>
<span class="secondary">
${item.attributes.friendly_name}
${displayName ?? deviceName ?? item.attributes.friendly_name}
</span>
</div>
</div>
@@ -184,6 +214,12 @@ class HaPanelDevStateRenderer extends LitElement {
<div class="cell" role="cell">
<span class="padded">${item.state}</span>
</div>
<div class="cell" role="cell">
<span class="padded">${deviceName ?? "\u2014"}</span>
</div>
<div class="cell" role="cell">
<span class="padded">${areaName ?? "\u2014"}</span>
</div>
<div class="cell" role="cell">
<span class="padded">${this._attributeString(item)}</span>
</div>
@@ -214,26 +250,29 @@ class HaPanelDevStateRenderer extends LitElement {
return output;
}
private _copyEntity = async (ev) => {
private _copyEntity = async (ev: Event) => {
ev.preventDefault();
const entity = (ev.currentTarget! as any).entity;
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
.entity;
await copyToClipboard(entity.entity_id, document.body);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
message: this._i18n.localize("ui.common.copied_clipboard"),
});
};
private _entityMoreInfo(ev) {
private _entityMoreInfo(ev: Event) {
ev.preventDefault();
const entity = (ev.currentTarget! as any).entity;
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
.entity;
fireEvent(this, "hass-more-info", { entityId: entity.entity_id });
}
private _entitySelected(ev) {
private _entitySelected(ev: Event) {
ev.preventDefault();
const entity = (ev.currentTarget! as any).entity;
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
.entity;
fireEvent(this, "states-tool-entity-selected", {
entity: entity,
entity,
});
}
@@ -307,12 +346,12 @@ class HaPanelDevStateRenderer extends LitElement {
flex: 2;
}
.entities .row .header:nth-child(3),
.entities .row .cell:nth-child(3) {
.entities .row .header:nth-child(5),
.entities .row .cell:nth-child(5) {
flex: 2;
}
.entities .row .cell:nth-child(3) {
.entities .row .cell:nth-child(5) {
white-space: pre-wrap;
}
@@ -320,8 +359,20 @@ class HaPanelDevStateRenderer extends LitElement {
display: none;
}
.hide-attributes .row .header:nth-child(3),
.hide-attributes .row .cell:nth-child(3) {
.hide-attributes .row .header:nth-child(5),
.hide-attributes .row .cell:nth-child(5) {
display: none;
}
.hide-extra .row .header:nth-child(3),
.hide-extra .row .cell:nth-child(3),
.hide-extra .row .header:nth-child(4),
.hide-extra .row .cell:nth-child(4) {
display: none;
}
.hide-extra .filter-devices,
.hide-extra .filter-areas {
display: none;
}
@@ -1,4 +1,5 @@
import { mdiContentCopy, mdiRefresh } from "@mdi/js";
import { consume, type ContextType } from "@lit/context";
import { addHours } from "date-fns";
import type {
HassEntities,
@@ -12,6 +13,8 @@ import memoizeOne from "memoize-one";
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
import { storage } from "../../../../common/decorators/storage";
import { escapeRegExp } from "../../../../common/string/escape_regexp";
import { computeAreaName } from "../../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-alert";
@@ -28,9 +31,16 @@ import "../../../../components/input/ha-input";
import type { HaInput } from "../../../../components/input/ha-input";
import "../../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../../components/input/ha-input-search";
import {
apiContext,
configContext,
internationalizationContext,
registriesContext,
statesContext,
} from "../../../../data/context";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { HomeAssistant, HomeAssistantRegistries } from "../../../../types";
import { showToast } from "../../../../util/toast";
import "./developer-tools-state-renderer";
@@ -55,6 +65,10 @@ class HaPanelDevState extends LitElement {
@state() private _attributeFilter = "";
@state() private _deviceFilter = "";
@state() private _areaFilter = "";
@state() private _entity?: HassEntity;
@state() private _state = "";
@@ -75,6 +89,26 @@ class HaPanelDevState extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries!: ContextType<typeof registriesContext>;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: ContextType<typeof statesContext>;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _filteredEntities = memoizeOne(
@@ -82,13 +116,23 @@ class HaPanelDevState extends LitElement {
entityFilter: string,
stateFilter: string,
attributeFilter: string,
states: HassEntities
deviceFilter: string,
areaFilter: string,
states: HassEntities,
entities: HomeAssistantRegistries["entities"],
devices: HomeAssistantRegistries["devices"],
areas: HomeAssistantRegistries["areas"]
): HassEntity[] =>
this._applyFiltersOnEntities(
entityFilter,
stateFilter,
attributeFilter,
states
deviceFilter,
areaFilter,
states,
entities,
devices,
areas
)
);
@@ -97,13 +141,18 @@ class HaPanelDevState extends LitElement {
this._entityFilter,
this._stateFilter,
this._attributeFilter,
this.hass.states
this._deviceFilter,
this._areaFilter,
this._states,
this._registries.entities,
this._registries.devices,
this._registries.areas
);
return html`
<div class="heading">
<h1>
${this.hass.localize(
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.current_entities"
)}
</h1>
@@ -112,14 +161,14 @@ class HaPanelDevState extends LitElement {
.checked=${this._showAttributes}
@change=${this._saveAttributeCheckboxState}
>
${this.hass.localize(
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.attributes"
)}
</ha-checkbox>`
: nothing}
</div>
<ha-expansion-panel
.header=${this.hass.localize(
.header=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.set_state"
)}
outlined
@@ -127,10 +176,10 @@ class HaPanelDevState extends LitElement {
@expanded-changed=${this._expandedChanged}
>
<p>
${this.hass.localize(
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.description1"
)}<br />
${this.hass.localize(
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.description2"
)}
</p>
@@ -153,7 +202,7 @@ class HaPanelDevState extends LitElement {
<ha-icon-button
.path=${mdiContentCopy}
@click=${this._copyStateEntity}
title=${this.hass.localize(
title=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.copy_id"
)}
></ha-icon-button>
@@ -161,7 +210,7 @@ class HaPanelDevState extends LitElement {
`
: nothing}
<ha-input
.label=${this.hass.localize(
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.state"
)}
required
@@ -174,7 +223,7 @@ class HaPanelDevState extends LitElement {
class="state-input"
></ha-input>
<p>
${this.hass.localize(
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.state_attributes"
)}
</p>
@@ -190,13 +239,13 @@ class HaPanelDevState extends LitElement {
@click=${this._handleSetState}
.disabled=${!this._validJSON}
raised
>${this.hass.localize(
>${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.set_state"
)}</ha-button
>
<ha-icon-button
@click=${this._updateEntity}
.label=${this.hass.localize("ui.common.refresh")}
.label=${this._i18n.localize("ui.common.refresh")}
.path=${mdiRefresh}
></ha-icon-button>
</div>
@@ -205,7 +254,7 @@ class HaPanelDevState extends LitElement {
${this._entity
? html`<p>
<b
>${this.hass.localize(
>${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.last_changed"
)}:</b
><br />
@@ -215,7 +264,7 @@ class HaPanelDevState extends LitElement {
</p>
<p>
<b
>${this.hass.localize(
>${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.last_updated"
)}:</b
><br />
@@ -228,7 +277,6 @@ class HaPanelDevState extends LitElement {
</div>
</ha-expansion-panel>
<developer-tools-state-renderer
.hass=${this.hass}
.narrow=${this.narrow}
.entities=${entities}
.virtualize=${entities.length > VIRTUALIZE_THRESHOLD}
@@ -237,7 +285,7 @@ class HaPanelDevState extends LitElement {
>
<ha-input-search
slot="filter-entities"
.label=${this.hass.localize(
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.filter_entities"
)}
.value=${this._entityFilter}
@@ -245,16 +293,32 @@ class HaPanelDevState extends LitElement {
></ha-input-search>
<ha-input-search
slot="filter-states"
.label=${this.hass.localize(
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.filter_states"
)}
type="search"
.value=${this._stateFilter}
@input=${this._stateFilterChanged}
></ha-input-search>
<ha-input-search
slot="filter-devices"
.label=${this._i18n.localize(
"ui.panel.config.entities.picker.headers.device"
)}
type="search"
.value=${this._deviceFilter}
@input=${this._deviceFilterChanged}
></ha-input-search>
<ha-input-search
slot="filter-areas"
.label=${this._i18n.localize("ui.panel.config.generic.headers.area")}
type="search"
.value=${this._areaFilter}
@input=${this._areaFilterChanged}
></ha-input-search>
<ha-input-search
slot="filter-attributes"
.label=${this.hass.localize(
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.filter_attributes"
)}
type="search"
@@ -269,7 +333,7 @@ class HaPanelDevState extends LitElement {
ev.preventDefault();
await copyToClipboard(this._entityId);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
message: this._i18n.localize("ui.common.copied_clipboard"),
});
}
@@ -296,7 +360,7 @@ class HaPanelDevState extends LitElement {
private _updateEntity() {
const entityState = this._entityId
? this.hass.states[this._entityId]
? this._states[this._entityId]
: undefined;
if (!entityState) {
this._entity = undefined;
@@ -328,6 +392,14 @@ class HaPanelDevState extends LitElement {
this._attributeFilter = (ev.target as HaInputSearch).value ?? "";
}
private _deviceFilterChanged(ev: InputEvent) {
this._deviceFilter = (ev.target as HaInputSearch).value ?? "";
}
private _areaFilterChanged(ev: InputEvent) {
this._areaFilter = (ev.target as HaInputSearch).value ?? "";
}
private _getHistoryURL(entityId, inputDate) {
const date = new Date(inputDate);
const hourBefore = addHours(date, -1).toISOString();
@@ -358,7 +430,7 @@ class HaPanelDevState extends LitElement {
this._error = "";
if (!this._entityId) {
showAlertDialog(this, {
text: this.hass.localize(
text: this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.alert_entity_field"
),
});
@@ -366,7 +438,7 @@ class HaPanelDevState extends LitElement {
}
this._updateEditor();
try {
await this.hass.callApi("POST", "states/" + this._entityId, {
await this._api.callApi("POST", "states/" + this._entityId, {
state: this._state,
attributes: this._stateAttributes,
});
@@ -379,7 +451,12 @@ class HaPanelDevState extends LitElement {
entityFilter: string,
stateFilter: string,
attributeFilter: string,
states: HassEntities
deviceFilter: string,
areaFilter: string,
states: HassEntities,
entities: HomeAssistantRegistries["entities"],
devices: HomeAssistantRegistries["devices"],
areas: HomeAssistantRegistries["areas"]
) {
const entityFilterRegExp =
entityFilter &&
@@ -389,6 +466,14 @@ class HaPanelDevState extends LitElement {
stateFilter &&
RegExp(escapeRegExp(stateFilter).replace(/\\\*/g, ".*"), "i");
const deviceFilterRegExp =
deviceFilter &&
RegExp(escapeRegExp(deviceFilter).replace(/\\\*/g, ".*"), "i");
const areaFilterRegExp =
areaFilter &&
RegExp(escapeRegExp(areaFilter).replace(/\\\*/g, ".*"), "i");
let keyFilterRegExp;
let valueFilterRegExp;
let multiMode = false;
@@ -428,6 +513,30 @@ class HaPanelDevState extends LitElement {
return false;
}
if (deviceFilterRegExp) {
const entry = entities[value.entity_id];
const device = entry?.device_id
? devices[entry.device_id]
: undefined;
const deviceName = device ? computeDeviceName(device) : undefined;
if (!deviceName || !deviceFilterRegExp.test(deviceName)) {
return false;
}
}
if (areaFilterRegExp) {
const entry = entities[value.entity_id];
const device = entry?.device_id
? devices[entry.device_id]
: undefined;
const areaId = entry?.area_id || device?.area_id;
const area = areaId ? areas[areaId] : undefined;
const areaName = area ? computeAreaName(area) : undefined;
if (!areaName || !areaFilterRegExp.test(areaName)) {
return false;
}
}
if (keyFilterRegExp && valueFilterRegExp) {
for (const [key, attributeValue] of Object.entries(
value.attributes
@@ -468,16 +577,16 @@ class HaPanelDevState extends LitElement {
private _lastChangedString(entity) {
return formatDateTimeWithSeconds(
new Date(entity.last_changed),
this.hass.locale,
this.hass.config
this._i18n.locale,
this._config.config
);
}
private _lastUpdatedString(entity) {
return formatDateTimeWithSeconds(
new Date(entity.last_updated),
this.hass.locale,
this.hass.config
this._i18n.locale,
this._config.config
);
}