Compare commits

...

7 Commits

Author SHA1 Message Date
karwosts a5bf35690b Add time format to entity badge (#52713) 2026-06-17 20:24:03 +02:00
karwosts d98eb47490 Decode supported features in more-info-details (#52712)
* Decode supported features in more-info-details

* Remove 'Supported features' translation entry
2026-06-17 18:33:18 +02:00
karwosts 738e92d27d Add time_format to tile card (#52450)
* Add time_format to tile card

* Updates

* incorrect type

* Apply suggestions from code review

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* code review feedback

* handle timestamp=0

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-06-17 16:04:40 +02:00
Aidan Timson ade2e9272b Reword advanced settings to more options in helpers (#52701) 2026-06-17 15:47:24 +02:00
Simon Lamon d8ce60dfb6 Add icons to live condition test (#52458)
Add icons to live condition
2026-06-17 16:41:15 +03:00
Aidan Timson db9374925e Migrate more info climate (+ related) to lazy context (#52694)
* Migrate more info climate (+ related) to lazy context

* Remove hass
2026-06-17 13:19:46 +00:00
Aidan Timson 1bcd1293c0 Reword "advanced concept" in event trigger/action descriptions (#52699) 2026-06-17 16:07:57 +03:00
56 changed files with 667 additions and 326 deletions
+9
View File
@@ -110,6 +110,15 @@ export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set([
"media_player",
]);
/** Domains that use a timestamp for state. */
export const TIMESTAMP_STATE_DOMAINS = [
"button",
"infrared",
"input_button",
"radio_frequency",
"scene",
];
/** Temperature units. */
export const UNIT_C = "°C";
export const UNIT_F = "°F";
-23
View File
@@ -1,23 +0,0 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { supportsFeature } from "./supports-feature";
export type FeatureClassNames<T extends number = number> = Partial<
Record<T, string>
>;
// Expects classNames to be an object mapping feature-bit -> className
export const featureClassNames = (
stateObj: HassEntity,
classNames: FeatureClassNames
) => {
if (!stateObj || !stateObj.attributes.supported_features) {
return "";
}
return Object.keys(classNames)
.map((feature) =>
supportsFeature(stateObj, Number(feature)) ? classNames[feature] : ""
)
.filter((attr) => attr !== "")
.join(" ");
};
+48
View File
@@ -0,0 +1,48 @@
import { AITaskEntityFeature } from "../../data/ai_task";
import { AlarmControlPanelEntityFeature } from "../../data/alarm_control_panel";
import { AssistSatelliteEntityFeature } from "../../data/assist_satellite";
import { CalendarEntityFeature } from "../../data/calendar";
import { ClimateEntityFeature } from "../../data/climate";
import { ConversationEntityFeature } from "../../data/conversation";
import { CoverEntityFeature } from "../../data/cover";
import { FanEntityFeature } from "../../data/fan";
import { HumidifierEntityFeature } from "../../data/humidifier";
import { LawnMowerEntityFeature } from "../../data/lawn_mower";
import { LightEntityFeature } from "../../data/light";
import { LockEntityFeature } from "../../data/lock";
import { MediaPlayerEntityFeature } from "../../data/media-player";
import { TodoListEntityFeature } from "../../data/todo";
import { UpdateEntityFeature } from "../../data/update";
import { VacuumEntityFeature } from "../../data/vacuum";
import { ValveEntityFeature } from "../../data/valve";
import { WaterHeaterEntityFeature } from "../../data/water_heater";
import { WeatherEntityFeature } from "../../data/weather";
export type FeatureEnum = Record<string | number, string | number>;
const DOMAIN_ENUMS = {
ai_task: AITaskEntityFeature,
alarm_control_panel: AlarmControlPanelEntityFeature,
assist_satellite: AssistSatelliteEntityFeature,
calendar: CalendarEntityFeature,
climate: ClimateEntityFeature,
conversation: ConversationEntityFeature,
cover: CoverEntityFeature,
fan: FanEntityFeature,
humidifier: HumidifierEntityFeature,
lawn_mower: LawnMowerEntityFeature,
light: LightEntityFeature,
lock: LockEntityFeature,
media_player: MediaPlayerEntityFeature,
todo: TodoListEntityFeature,
update: UpdateEntityFeature,
vacuum: VacuumEntityFeature,
valve: ValveEntityFeature,
water_heater: WaterHeaterEntityFeature,
weather: WeatherEntityFeature,
};
export function getFeatures(domain: string): FeatureEnum | undefined {
const enumObj = DOMAIN_ENUMS[domain] as FeatureEnum;
return enumObj;
}
@@ -1,5 +1,12 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-svg-icon";
import {
mdiAlertCircle,
mdiCheckCircle,
mdiCloseCircle,
mdiHelpCircle,
} from "@mdi/js";
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
@@ -19,46 +26,59 @@ export class HaAutomationRowLiveTest extends LitElement {
@property() public label = "";
private get _iconPath() {
switch (this.state) {
case "pass":
return mdiCheckCircle;
case "fail":
return mdiCloseCircle;
case "invalid":
return mdiAlertCircle;
default:
return mdiHelpCircle;
}
}
protected render() {
return html`
<div
id="indicator"
role="status"
tabindex="0"
aria-label=${this.label}
></div>
<div id="indicator" role="status" tabindex="0" aria-label=${this.label}>
<ha-svg-icon .path=${this._iconPath}></ha-svg-icon>
</div>
`;
}
static styles = css`
:host {
position: absolute;
top: -5px;
inset-inline-end: -6px;
top: -8px;
inset-inline-end: -8px;
display: inline-block;
}
#indicator {
width: 10px;
height: 10px;
width: 16px;
height: 16px;
display: grid;
place-items: center;
border-radius: var(--ha-border-radius-circle);
border: var(--ha-border-width-md) solid;
box-sizing: border-box;
background-color: var(--card-background-color);
box-shadow: 0 0 0 2px var(--card-background-color);
transition: all var(--ha-animation-duration-normal) ease-in-out;
}
#indicator ha-svg-icon {
width: 16px;
height: 16px;
--mdc-icon-size: 16px;
}
:host([state="pass"]) #indicator {
background-color: var(--ha-color-green-60);
border-color: var(--ha-color-green-60);
color: var(--ha-color-green-60);
}
:host([state="fail"]) #indicator {
border-color: var(--ha-color-orange-60);
color: var(--ha-color-orange-60);
}
:host([state="invalid"]) #indicator {
border-color: var(--ha-color-red-60);
color: var(--ha-color-red-60);
}
:host([state="unknown"]) #indicator {
border-color: var(--ha-color-neutral-60);
color: var(--ha-color-neutral-60);
}
`;
}
+24 -6
View File
@@ -1,16 +1,20 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import {
configContext,
connectionContext,
entitiesContext,
} from "../data/context";
import { attributeIcon } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-attribute-icon")
export class HaAttributeIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@property() public attribute?: string;
@@ -19,6 +23,18 @@ export class HaAttributeIcon extends LitElement {
@property() public icon?: string;
@state()
@consume({ context: configContext, subscribe: true })
private _config?: ContextType<typeof configContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities?: ContextType<typeof entitiesContext>;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -28,12 +44,14 @@ export class HaAttributeIcon extends LitElement {
return nothing;
}
if (!this.hass) {
if (!this._config || !this._connection || !this._entities) {
return nothing;
}
const icon = attributeIcon(
this.hass,
this._config.config,
this._connection.connection,
this._entities,
this.stateObj,
this.attribute,
this.attributeValue
+11 -12
View File
@@ -1,10 +1,12 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { formatNumber } from "../common/number/format_number";
import { blankBeforeUnit } from "../common/translations/blank_before_unit";
import type { HomeAssistant } from "../types";
import { internationalizationContext } from "../data/context";
@customElement("ha-big-number")
export class HaBigNumber extends LitElement {
@@ -15,17 +17,16 @@ export class HaBigNumber extends LitElement {
@property({ attribute: "unit-position" })
public unitPosition: "top" | "bottom" = "top";
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false })
public formatOptions: Intl.NumberFormatOptions = {};
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
protected render() {
const formatted = formatNumber(
this.value,
this.hass?.locale,
this.formatOptions
);
const locale = this._i18n!.locale;
const formatted = formatNumber(this.value, locale, this.formatOptions);
const [integer] = formatted.includes(".")
? formatted.split(".")
: formatted.split(",");
@@ -33,9 +34,7 @@ export class HaBigNumber extends LitElement {
const temperatureDecimal = formatted.replace(integer, "");
const formattedValue = `${this.value}${
this.unit
? `${blankBeforeUnit(this.unit, this.hass?.locale)}${this.unit}`
: ""
this.unit ? `${blankBeforeUnit(this.unit, locale)}${this.unit}` : ""
}`;
const unitBottom = this.unitPosition === "bottom";
@@ -0,0 +1,32 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-time-format-picker";
@customElement("ha-selector-ui_time_format")
export class HaSelectorUiTimeFormat extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`
<ha-time-format-picker
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
>
</ha-time-format-picker>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-ui_time_format": HaSelectorUiTimeFormat;
}
}
@@ -67,6 +67,7 @@ const LOAD_ELEMENTS = {
ui_action: () => import("./ha-selector-ui-action"),
ui_color: () => import("./ha-selector-ui-color"),
ui_state_content: () => import("./ha-selector-ui-state-content"),
ui_time_format: () => import("./ha-selector-ui-time-format"),
};
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
+67
View File
@@ -0,0 +1,67 @@
import memoizeOne from "memoize-one";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import "./ha-select";
import { TIMESTAMP_RENDERING_FORMATS } from "../panels/lovelace/components/types";
@customElement("ha-time-format-picker")
export class HaTimeFormatPicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
private _options = memoizeOne((localize: LocalizeFunc) =>
[{ label: localize("ui.common.auto"), value: "auto" }].concat(
TIMESTAMP_RENDERING_FORMATS.map((format) => ({
label:
localize(`ui.components.time-format-picker.formats.${format}`) ||
format,
value: format,
}))
)
);
protected render() {
return html`
<ha-select
.label=${this.label ?? ""}
.value=${this.value || "auto"}
.helper=${this.helper ?? ""}
.disabled=${this.disabled}
@selected=${this._selectChanged}
.options=${this._options(this._localize)}
>
</ha-select>
`;
}
private _selectChanged(ev) {
ev.stopPropagation();
if (ev.detail?.value === "auto" && this.value !== undefined) {
fireEvent(this, "value-changed", {
value: undefined,
});
return;
}
fireEvent(this, "value-changed", {
value: ev.detail.value,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-time-format-picker": HaTimeFormatPicker;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import type { HomeAssistant } from "../types";
import type { Selector } from "./selector";
export const enum AITaskEntityFeature {
export enum AITaskEntityFeature {
GENERATE_DATA = 1,
SUPPORT_ATTACHMENTS = 2,
GENERATE_IMAGE = 4,
+1 -1
View File
@@ -18,7 +18,7 @@ import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
export const FORMAT_TEXT = "text";
export const FORMAT_NUMBER = "number";
export const enum AlarmControlPanelEntityFeature {
export enum AlarmControlPanelEntityFeature {
ARM_HOME = 1,
ARM_AWAY = 2,
ARM_NIGHT = 4,
+1 -1
View File
@@ -3,7 +3,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export const enum AssistSatelliteEntityFeature {
export enum AssistSatelliteEntityFeature {
ANNOUNCE = 1,
}
+1 -1
View File
@@ -54,7 +54,7 @@ export enum RecurrenceRange {
THISANDFUTURE = "THISANDFUTURE",
}
export const enum CalendarEntityFeature {
export enum CalendarEntityFeature {
CREATE_EVENT = 1,
DELETE_EVENT = 2,
UPDATE_EVENT = 4,
+1 -1
View File
@@ -68,7 +68,7 @@ export type ClimateEntity = HassEntityBase & {
};
};
export const enum ClimateEntityFeature {
export enum ClimateEntityFeature {
TARGET_TEMPERATURE = 1,
TARGET_TEMPERATURE_RANGE = 2,
TARGET_HUMIDITY = 4,
+1 -1
View File
@@ -1,7 +1,7 @@
import { ensureArray } from "../common/array/ensure-array";
import type { HomeAssistant } from "../types";
export const enum ConversationEntityFeature {
export enum ConversationEntityFeature {
CONTROL = 1,
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistantFormatters } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export const enum CoverEntityFeature {
export enum CoverEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
+1 -1
View File
@@ -12,7 +12,7 @@ import type {
import { stateActive } from "../common/entity/state_active";
import type { HomeAssistant } from "../types";
export const enum FanEntityFeature {
export enum FanEntityFeature {
SET_SPEED = 1,
OSCILLATE = 2,
DIRECTION = 4,
+1 -1
View File
@@ -20,7 +20,7 @@ export type HumidifierEntity = HassEntityBase & {
};
};
export const enum HumidifierEntityFeature {
export enum HumidifierEntityFeature {
MODES = 1,
}
+8 -6
View File
@@ -548,7 +548,9 @@ const getEntityIcon = async (
};
export const attributeIcon = async (
hass: HomeAssistant,
hassConfig: HomeAssistant["config"],
hassConnection: HomeAssistant["connection"],
entities: HomeAssistant["entities"],
state: HassEntity,
attribute: string,
attributeValue?: string
@@ -556,7 +558,7 @@ export const attributeIcon = async (
let icon: string | undefined;
const domain = computeStateDomain(state);
const deviceClass = state.attributes.device_class;
const entity = hass.entities?.[state.entity_id] as
const entity = entities[state.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
const platform = entity?.platform;
@@ -567,8 +569,8 @@ export const attributeIcon = async (
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(
hass.config,
hass.connection,
hassConfig,
hassConnection,
platform
);
if (platformIcons) {
@@ -580,8 +582,8 @@ export const attributeIcon = async (
}
if (!icon) {
const entityComponentIcons = await getComponentIcons(
hass.connection,
hass.config,
hassConnection,
hassConfig,
domain
);
if (entityComponentIcons) {
+1 -1
View File
@@ -11,7 +11,7 @@ export type LawnMowerEntityState =
| "docked"
| "error";
export const enum LawnMowerEntityFeature {
export enum LawnMowerEntityFeature {
START_MOWING = 1,
PAUSE = 2,
DOCK = 4,
+1 -1
View File
@@ -4,7 +4,7 @@ import type {
} from "home-assistant-js-websocket";
import { temperature2rgb } from "../common/color/convert-light-color";
export const enum LightEntityFeature {
export enum LightEntityFeature {
EFFECT = 4,
FLASH = 8,
TRANSITION = 32,
+1 -1
View File
@@ -7,7 +7,7 @@ import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
export const enum LockEntityFeature {
export enum LockEntityFeature {
OPEN = 1,
}
+1 -1
View File
@@ -82,7 +82,7 @@ export interface MediaPlayerEntity extends HassEntityBase {
| "buffering";
}
export const enum MediaPlayerEntityFeature {
export enum MediaPlayerEntityFeature {
PAUSE = 1,
SEEK = 2,
VOLUME_SET = 4,
+5
View File
@@ -82,6 +82,7 @@ export type Selector =
| UiActionSelector
| UiColorSelector
| UiStateContentSelector
| UiTimeFormatSelector
| BackupLocationSelector;
export interface ActionSelector {
@@ -601,6 +602,10 @@ export interface UiStateContentSelector {
} | null;
}
export interface UiTimeFormatSelector {
ui_time_format: {} | null;
}
export interface EntityNameSelector {
entity_name: {
entity_id?: string;
+1 -1
View File
@@ -31,7 +31,7 @@ export interface TodoItem {
completed?: string | null;
}
export const enum TodoListEntityFeature {
export enum TodoListEntityFeature {
CREATE_TODO_ITEM = 1,
DELETE_TODO_ITEM = 2,
UPDATE_TODO_ITEM = 4,
+1 -1
View File
@@ -15,7 +15,7 @@ export type VacuumEntityState =
| "returning"
| "error";
export const enum VacuumEntityFeature {
export enum VacuumEntityFeature {
TURN_ON = 1,
TURN_OFF = 2,
PAUSE = 4,
+1 -1
View File
@@ -7,7 +7,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
import type { HomeAssistantFormatters } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export const enum ValveEntityFeature {
export enum ValveEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
+1 -1
View File
@@ -3,7 +3,7 @@ import type {
HassEntityBase,
} from "home-assistant-js-websocket";
export const enum WaterHeaterEntityFeature {
export enum WaterHeaterEntityFeature {
TARGET_TEMPERATURE = 1,
OPERATION_MODE = 2,
AWAY_MODE = 4,
+1 -1
View File
@@ -41,7 +41,7 @@ import { round } from "../common/number/round";
import "../components/ha-svg-icon";
import type { HomeAssistant } from "../types";
export const enum WeatherEntityFeature {
export enum WeatherEntityFeature {
FORECAST_DAILY = 1,
FORECAST_HOURLY = 2,
FORECAST_TWICE_DAILY = 4,
@@ -1,3 +1,5 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import {
mdiArrowOscillating,
mdiFan,
@@ -8,7 +10,9 @@ import {
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group";
@@ -22,10 +26,10 @@ import {
climateHvacModeIcon,
compareClimateHvacModes,
} from "../../../data/climate";
import { apiContext, formattersContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import "../../../state-control/climate/ha-state-control-climate-humidity";
import "../../../state-control/climate/ha-state-control-climate-temperature";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -34,15 +38,24 @@ type MainControl = "temperature" | "humidity";
@customElement("more-info-climate")
class MoreInfoClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: ClimateEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state() private _mainControl: MainControl = "temperature";
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
@@ -50,7 +63,6 @@ class MoreInfoClimate extends LitElement {
private _renderFanModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="fan_mode"
.attributeValue=${value}
@@ -58,7 +70,6 @@ class MoreInfoClimate extends LitElement {
private _renderSwingModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_mode"
.attributeValue=${value}
@@ -66,7 +77,6 @@ class MoreInfoClimate extends LitElement {
private _renderSwingHorizontalModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${value}
@@ -120,13 +130,13 @@ class MoreInfoClimate extends LitElement {
? html`
<div>
<p class="label">
${this.hass.formatEntityAttributeName(
${this._formatters.formatEntityAttributeName(
this.stateObj,
"current_temperature"
)}
</p>
<p class="value">
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
)}
@@ -138,13 +148,13 @@ class MoreInfoClimate extends LitElement {
? html`
<div>
<p class="label">
${this.hass.formatEntityAttributeName(
${this._formatters.formatEntityAttributeName(
this.stateObj,
"current_humidity"
)}
</p>
<p class="value">
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}
@@ -157,7 +167,6 @@ class MoreInfoClimate extends LitElement {
${this._mainControl === "temperature"
? html`
<ha-state-control-climate-temperature
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-climate-temperature>
`
@@ -165,7 +174,6 @@ class MoreInfoClimate extends LitElement {
${this._mainControl === "humidity"
? html`
<ha-state-control-climate-humidity
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-climate-humidity>
`
@@ -176,7 +184,7 @@ class MoreInfoClimate extends LitElement {
<ha-icon-button-toggle
.selected=${this._mainControl === "temperature"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.more_info_control.climate.temperature"
)}
.control=${"temperature"}
@@ -187,7 +195,7 @@ class MoreInfoClimate extends LitElement {
<ha-icon-button-toggle
.selected=${this._mainControl === "humidity"}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize(
.label=${this._localize(
"ui.dialogs.more_info_control.climate.humidity"
)}
.control=${"humidity"}
@@ -201,8 +209,7 @@ class MoreInfoClimate extends LitElement {
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.localize("ui.card.climate.mode")}
.label=${this._localize("ui.card.climate.mode")}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
.options=${stateObj.attributes.hvac_modes
@@ -211,7 +218,7 @@ class MoreInfoClimate extends LitElement {
.map((mode) => ({
value: mode,
iconPath: climateHvacModeIcon(mode),
label: this.hass.formatEntityState(stateObj, mode),
label: this._formatters.formatEntityState(stateObj, mode),
}))}
@wa-select=${this._handleOperationModeChanged}
>
@@ -223,8 +230,7 @@ class MoreInfoClimate extends LitElement {
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
stateObj,
"preset_mode"
)}
@@ -233,7 +239,7 @@ class MoreInfoClimate extends LitElement {
@wa-select=${this._handlePresetmodeChanged}
.options=${stateObj.attributes.preset_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
@@ -248,8 +254,7 @@ class MoreInfoClimate extends LitElement {
${supportFanMode && stateObj.attributes.fan_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
stateObj,
"fan_mode"
)}
@@ -258,7 +263,7 @@ class MoreInfoClimate extends LitElement {
@wa-select=${this._handleFanModeChanged}
.options=${stateObj.attributes.fan_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
@@ -273,8 +278,7 @@ class MoreInfoClimate extends LitElement {
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
stateObj,
"swing_mode"
)}
@@ -283,7 +287,7 @@ class MoreInfoClimate extends LitElement {
@wa-select=${this._handleSwingmodeChanged}
.options=${stateObj.attributes.swing_modes.map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
@@ -302,8 +306,7 @@ class MoreInfoClimate extends LitElement {
stateObj.attributes.swing_horizontal_modes
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
stateObj,
"swing_horizontal_mode"
)}
@@ -313,7 +316,7 @@ class MoreInfoClimate extends LitElement {
.options=${stateObj.attributes.swing_horizontal_modes.map(
(mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
stateObj,
"swing_horizontal_mode",
mode
@@ -403,7 +406,7 @@ class MoreInfoClimate extends LitElement {
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this.hass.callService("climate", service, data);
await this._api.callService("climate", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
@@ -42,7 +42,6 @@ class MoreInfoFan extends LitElement {
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
@@ -50,7 +49,6 @@ class MoreInfoFan extends LitElement {
private _renderDirectionIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${value}
@@ -241,7 +239,6 @@ class MoreInfoFan extends LitElement {
>
<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${this.stateObj.attributes.direction}
@@ -1,31 +1,44 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiPower, mdiTuneVariant } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-list-item";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { apiContext, formattersContext } from "../../../data/context";
import type { HumidifierEntity } from "../../../data/humidifier";
import { HumidifierEntityFeature } from "../../../data/humidifier";
import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("more-info-humidifier")
class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HumidifierEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state() public _mode?: string;
private _renderModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="mode"
.attributeValue=${value}
@@ -43,7 +56,6 @@ class MoreInfoHumidifier extends LitElement {
return nothing;
}
const hass = this.hass;
const stateObj = this.stateObj;
const supportModes = supportsFeature(
@@ -57,13 +69,13 @@ class MoreInfoHumidifier extends LitElement {
? html`
<div>
<p class="label">
${this.hass.formatEntityAttributeName(
${this._formatters.formatEntityAttributeName(
this.stateObj,
"current_humidity"
)}
</p>
<p class="value">
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}
@@ -75,22 +87,20 @@ class MoreInfoHumidifier extends LitElement {
<div class="controls">
<ha-state-control-humidifier-humidity
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-humidifier-humidity>
</div>
<ha-more-info-control-select-container>
<ha-control-select-menu
.hass=${hass}
.label=${this.hass.localize("ui.card.humidifier.state")}
.label=${this._localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleStateChanged}
.options=${["off", "on"].map((fanState) => ({
value: fanState,
label: this.stateObj
? this.hass.formatEntityState(this.stateObj, fanState)
? this._formatters.formatEntityState(this.stateObj, fanState)
: fanState,
}))}
>
@@ -100,15 +110,14 @@ class MoreInfoHumidifier extends LitElement {
${supportModes
? html`
<ha-control-select-menu
.hass=${hass}
.label=${hass.localize("ui.card.humidifier.mode")}
.label=${this._localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
@wa-select=${this._handleModeChanged}
.options=${stateObj.attributes.available_modes?.map((mode) => ({
value: mode,
label: stateObj
? this.hass.formatEntityAttributeValue(
? this._formatters.formatEntityAttributeValue(
stateObj,
"mode",
mode
@@ -164,7 +173,7 @@ class MoreInfoHumidifier extends LitElement {
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this.hass.callService("humidifier", service, data);
await this._api.callService("humidifier", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
@@ -60,7 +60,6 @@ class MoreInfoLight extends LitElement {
private _renderEffectIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${value}
@@ -1,8 +1,12 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiAccount, mdiAccountArrowRight, mdiWaterBoiler } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -13,20 +17,29 @@ import {
WaterHeaterEntityFeature,
compareWaterHeaterOperationMode,
} from "../../../data/water_heater";
import { apiContext, formattersContext } from "../../../data/context";
import "../../../state-control/water_heater/ha-state-control-water_heater-temperature";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-water_heater")
class MoreInfoWaterHeater extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: WaterHeaterEntity;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
private _renderOperationModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="operation_mode"
.attributeValue=${value}
@@ -57,13 +70,13 @@ class MoreInfoWaterHeater extends LitElement {
? html`
<div>
<p class="label">
${this.hass.formatEntityAttributeName(
${this._formatters.formatEntityAttributeName(
this.stateObj,
"current_temperature"
)}
</p>
<p class="value">
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
)}
@@ -74,7 +87,6 @@ class MoreInfoWaterHeater extends LitElement {
</div>
<div class="controls">
<ha-state-control-water_heater-temperature
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-state-control-water_heater-temperature>
</div>
@@ -82,8 +94,7 @@ class MoreInfoWaterHeater extends LitElement {
${supportOperationMode && stateObj.attributes.operation_list
? html`
<ha-control-select-menu
.hass=${this.hass}
.label=${this.hass.localize("ui.card.water_heater.mode")}
.label=${this._localize("ui.card.water_heater.mode")}
.value=${stateObj.state}
.disabled=${stateObj.state === UNAVAILABLE}
@wa-select=${this._handleOperationModeChanged}
@@ -92,7 +103,7 @@ class MoreInfoWaterHeater extends LitElement {
.sort(compareWaterHeaterOperationMode)
.map((mode) => ({
value: mode,
label: this.hass.formatEntityState(stateObj, mode),
label: this._formatters.formatEntityState(stateObj, mode),
}))}
.renderIcon=${this._renderOperationModeIcon}
>
@@ -103,7 +114,7 @@ class MoreInfoWaterHeater extends LitElement {
${supportAwayMode
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
.label=${this._formatters.formatEntityAttributeName(
stateObj,
"away_mode"
)}
@@ -112,7 +123,7 @@ class MoreInfoWaterHeater extends LitElement {
@wa-select=${this._handleAwayModeChanged}
.options=${["on", "off"].map((mode) => ({
value: mode,
label: this.hass.formatEntityAttributeValue(
label: this._formatters.formatEntityAttributeValue(
stateObj,
"away_mode",
mode
@@ -165,7 +176,7 @@ class MoreInfoWaterHeater extends LitElement {
data.entity_id = this.stateObj!.entity_id;
const curState = this.stateObj;
await this.hass.callService("water_heater", service, data);
await this._api.callService("water_heater", service, data);
// We reset stateObj to re-sync the inputs with the state. It will be out
// of sync if our service call did not result in the entity to be turned
+36 -4
View File
@@ -13,6 +13,11 @@ import { computeShownAttributes } from "../../data/entity/entity_attributes";
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import "../../components/ha-yaml-editor";
import { computeDomain } from "../../common/entity/compute_domain";
import type { FeatureEnum } from "../../common/entity/get_domain_features";
import { getFeatures } from "../../common/entity/get_domain_features";
import { supportsFeature } from "../../common/entity/supports-feature";
import { titleCase } from "../../common/string/title-case";
interface DetailsViewParams {
entityId: string;
@@ -177,6 +182,12 @@ class HaMoreInfoDetails extends LitElement {
</div>`;
}
let featureEnum: FeatureEnum | undefined;
if (this._stateObj?.attributes.supported_features !== undefined) {
const domain = computeDomain(this.params!.entityId);
featureEnum = getFeatures(domain);
}
return attributes.map(
(attribute) => html`
<div class="data-entry">
@@ -189,16 +200,37 @@ class HaMoreInfoDetails extends LitElement {
)}
</div>
<div class="value">
<ha-attribute-value
.attribute=${attribute}
.stateObj=${this._stateObj}
></ha-attribute-value>
${attribute === "supported_features" && featureEnum
? this._renderFeatures(featureEnum, this._stateObj!)
: html`
<ha-attribute-value
.attribute=${attribute}
.stateObj=${this._stateObj}
></ha-attribute-value>
`}
</div>
</div>
`
);
}
private _renderFeatures(
featureEnum: FeatureEnum,
stateObj: HassEntity
): string {
return (
Object.entries(featureEnum)
.filter(([_key, value]) => typeof value === "number")
.map(([key, value]) =>
supportsFeature(stateObj, value as number)
? titleCase(key.replaceAll("_", "\u00A0").toLowerCase())
: undefined
)
.filter(Boolean)
.join(", ") || this.hass.localize("ui.common.none")
);
}
static styles: CSSResultGroup = css`
:host {
display: flex;
@@ -44,7 +44,7 @@ class DialogScheduleBlockInfo extends DirtyStateProviderMixin<ScheduleBlockInfo>
selector: { time: { no_second: true } },
},
{
name: "advanced_settings",
name: "more_options",
type: "expandable" as const,
flatten: true,
expanded: expand,
@@ -157,9 +157,9 @@ class DialogScheduleBlockInfo extends DirtyStateProviderMixin<ScheduleBlockInfo>
return this.hass!.localize("ui.dialogs.helper_settings.schedule.end");
case "data":
return this.hass!.localize("ui.dialogs.helper_settings.schedule.data");
case "advanced_settings":
case "more_options":
return this.hass!.localize(
"ui.dialogs.helper_settings.generic.advanced_settings"
"ui.dialogs.helper_settings.generic.more_options"
);
}
return "";
@@ -125,7 +125,7 @@ class HaCounterForm extends LitElement {
></ha-input>
<ha-expansion-panel
header=${this.hass.localize(
"ui.dialogs.helper_settings.generic.advanced_settings"
"ui.dialogs.helper_settings.generic.more_options"
)}
outlined
>
@@ -93,7 +93,7 @@ class HaInputTextForm extends LitElement {
></ha-icon-picker>
<ha-expansion-panel
header=${this.hass.localize(
"ui.dialogs.helper_settings.generic.advanced_settings"
"ui.dialogs.helper_settings.generic.more_options"
)}
outlined
>
@@ -181,6 +181,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.timeFormat=${this._config.time_format}
.name=${name}
>
</state-display>
+2
View File
@@ -5,6 +5,7 @@ import type { LegacyStateFilter } from "../common/evaluate-filter";
import type { Condition } from "../common/validate-condition";
import type { EntityFilterEntityConfig } from "../entity-rows/types";
import type { DisplayType } from "./hui-entity-badge";
import type { TimestampRenderingFormat } from "../components/types";
export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
type: "entity-filter";
@@ -53,6 +54,7 @@ export interface EntityBadgeConfig extends LovelaceBadgeConfig {
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
time_format?: TimestampRenderingFormat;
/**
* @deprecated use `show_state`, `show_name`, `icon_type`
*/
@@ -210,7 +210,6 @@ export abstract class HuiModeSelectCardFeatureBase<
protected _renderOptionIcon(option: HuiModeSelectOption): TemplateResult<1> {
return html`<ha-attribute-icon
slot="graphic"
.hass=${this.hass!}
.stateObj=${this._stateObj}
.attribute=${this._attribute}
.attributeValue=${option.value}
@@ -219,7 +218,6 @@ export abstract class HuiModeSelectCardFeatureBase<
private _renderMenuIcon = (value: string): TemplateResult<1> =>
html`<ha-attribute-icon
.hass=${this.hass!}
.stateObj=${this._stateObj}
.attribute=${this._attribute}
.attributeValue=${value}
@@ -151,7 +151,6 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
prevent-interaction-on-scroll
.showCurrentAsPrimary=${this._config.show_current_as_primary}
show-secondary
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-control-humidifier-humidity>
</div>
@@ -150,7 +150,6 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
})}
prevent-interaction-on-scroll
show-current
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-control-water_heater-temperature>`
: html` <ha-state-control-climate-temperature
@@ -160,7 +159,6 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
prevent-interaction-on-scroll
.showCurrentAsPrimary=${this._config.show_current_as_primary}
show-secondary
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-control-climate-temperature>`}
</div>
@@ -264,6 +264,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.timeFormat=${this._config.time_format}
>
</state-display>
`;
@@ -7,7 +7,7 @@ import type { ClimateEntity } from "../../../../../data/climate";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../../../data/climate";
import type { RenderBadgeFunction } from "./tile-badge";
export const renderClimateBadge: RenderBadgeFunction = (stateObj, hass) => {
export const renderClimateBadge: RenderBadgeFunction = (stateObj) => {
const hvacAction = (stateObj as ClimateEntity).attributes.hvac_action;
if (!hvacAction || hvacAction === "off") {
@@ -23,11 +23,7 @@ export const renderClimateBadge: RenderBadgeFunction = (stateObj, hass) => {
),
})}
>
<ha-attribute-icon
.hass=${hass}
.stateObj=${stateObj}
attribute="hvac_action"
>
<ha-attribute-icon .stateObj=${stateObj} attribute="hvac_action">
</ha-attribute-icon>
</ha-tile-badge>
`;
@@ -7,7 +7,7 @@ import type { HumidifierEntity } from "../../../../../data/humidifier";
import { HUMIDIFIER_ACTION_MODE } from "../../../../../data/humidifier";
import type { RenderBadgeFunction } from "./tile-badge";
export const renderHumidifierBadge: RenderBadgeFunction = (stateObj, hass) => {
export const renderHumidifierBadge: RenderBadgeFunction = (stateObj) => {
const action = (stateObj as HumidifierEntity).attributes.action;
if (!action || action === "off") {
@@ -23,7 +23,7 @@ export const renderHumidifierBadge: RenderBadgeFunction = (stateObj, hass) => {
),
})}
>
<ha-attribute-icon .hass=${hass} .stateObj=${stateObj} attribute="action">
<ha-attribute-icon .stateObj=${stateObj} attribute="action">
</ha-attribute-icon>
</ha-tile-badge>
`;
+1
View File
@@ -666,6 +666,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
icon_double_tap_action?: ActionConfig;
features?: LovelaceCardFeatureConfig[];
features_position?: LovelaceCardFeaturePosition;
time_format?: TimestampRenderingFormat;
}
export interface HeadingCardConfig extends LovelaceCardConfig {
@@ -33,6 +33,8 @@ import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types";
import { stateContentHasTimestamp } from "../../../../state-display/state-display";
import "./hui-card-features-editor";
const badgeConfigStruct = assign(
@@ -51,6 +53,7 @@ const badgeConfigStruct = assign(
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
time_format: optional(enums(TIMESTAMP_RENDERING_FORMATS)),
image: optional(string()), // For old badge config support
})
);
@@ -73,7 +76,7 @@ export class HuiEntityBadgeEditor
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
(localize: LocalizeFunc, showTimeFormat: boolean) =>
[
{ name: "entity", selector: { entity: {} } },
{
@@ -157,6 +160,16 @@ export class HuiEntityBadgeEditor
filter_entity: "entity",
},
},
...(showTimeFormat
? ([
{
name: "time_format",
selector: {
ui_time_format: {},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
],
},
{
@@ -214,7 +227,17 @@ export class HuiEntityBadgeEditor
return nothing;
}
const schema = this._schema(this.hass!.localize);
const entityId = this._config!.entity;
const showTimeFormat =
!!entityId &&
stateContentHasTimestamp(
entityId,
this.hass.states[entityId],
this._config.state_content
);
const schema = this._schema(this.hass!.localize, showTimeFormat);
const data = {
...this._config,
@@ -40,6 +40,8 @@ import { entityNameStruct } from "../structs/entity-name-struct";
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import { getSupportedFeaturesType } from "./hui-card-features-editor";
import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types";
import { stateContentHasTimestamp } from "../../../../state-display/state-display";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -60,6 +62,7 @@ const cardConfigStruct = assign(
icon_double_tap_action: optional(actionConfigStruct),
features: optional(array(any())),
features_position: optional(enums(["bottom", "inline"])),
time_format: optional(enums(TIMESTAMP_RENDERING_FORMATS)),
})
);
@@ -89,7 +92,8 @@ export class HuiTileCardEditor
(
localize: LocalizeFunc,
entityId: string | undefined,
hideState: boolean
hideState: boolean,
showTimeFormat: boolean
) =>
[
{ name: "entity", selector: { entity: {} } },
@@ -155,6 +159,16 @@ export class HuiTileCardEditor
},
] as const satisfies readonly HaFormSchema[])
: []),
...(showTimeFormat
? ([
{
name: "time_format",
selector: {
ui_time_format: {},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
{
name: "content_layout",
required: true,
@@ -271,10 +285,19 @@ export class HuiTileCardEditor
const entityId = this._config!.entity;
const showTimeFormat =
!this._config.hide_state &&
stateContentHasTimestamp(
entityId,
this.hass.states[entityId],
this._config.state_content
);
const schema = this._schema(
this.hass.localize,
entityId,
this._config.hide_state ?? false
this._config.hide_state ?? false,
showTimeFormat
);
const vertical = this._config.vertical ?? false;
@@ -1,3 +1,5 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiMinus, mdiPlus, mdiWaterPercent } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, html } from "lit";
@@ -8,6 +10,8 @@ import { stateActive } from "../../common/entity/state_active";
import { domainStateColorProperties } from "../../common/entity/state_color";
import { supportsFeature } from "../../common/entity/supports-feature";
import { clamp } from "../../common/number/clamp";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce";
import "../../components/ha-big-number";
import "../../components/ha-control-circular-slider";
@@ -15,9 +19,9 @@ import "../../components/ha-outlined-icon-button";
import "../../components/ha-svg-icon";
import type { ClimateEntity } from "../../data/climate";
import { ClimateEntityFeature } from "../../data/climate";
import { apiContext, formattersContext } from "../../data/context";
import { UNAVAILABLE } from "../../data/entity/entity";
import { computeCssVariable } from "../../resources/css-variables";
import type { HomeAssistant } from "../../types";
import {
createStateControlCircularSliderController,
stateControlCircularSliderStyle,
@@ -25,10 +29,20 @@ import {
@customElement("ha-state-control-climate-humidity")
export class HaStateControlClimateHumidity extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ClimateEntity;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: "show-current", type: Boolean })
public showCurrent = false;
@@ -74,7 +88,7 @@ export class HaStateControlClimateHumidity extends LitElement {
private _debouncedCallService = debounce(() => this._callService(), 1000);
private _callService() {
this.hass.callService("climate", "set_humidity", {
this._api.callService("climate", "set_humidity", {
entity_id: this.stateObj!.entity_id,
humidity: this._targetHumidity,
});
@@ -95,21 +109,21 @@ export class HaStateControlClimateHumidity extends LitElement {
if (this.stateObj.state === UNAVAILABLE) {
return html`
<p class="label disabled">
${this.hass.formatEntityState(this.stateObj, UNAVAILABLE)}
${this._formatters.formatEntityState(this.stateObj, UNAVAILABLE)}
</p>
`;
}
if (!this._targetHumidity) {
return html`
<p class="label">${this.hass.formatEntityState(this.stateObj)}</p>
<p class="label">
${this._formatters.formatEntityState(this.stateObj)}
</p>
`;
}
return html`
<p class="label">
${this.hass.localize("ui.card.climate.humidity_target")}
</p>
<p class="label">${this._localize("ui.card.climate.humidity_target")}</p>
`;
}
@@ -142,7 +156,6 @@ export class HaStateControlClimateHumidity extends LitElement {
.value=${humidity}
unit="%"
unit-position="bottom"
.hass=${this.hass}
.formatOptions=${formatOptions}
></ha-big-number>
`;
@@ -157,7 +170,7 @@ export class HaStateControlClimateHumidity extends LitElement {
<p class="label">
<ha-svg-icon .path=${mdiWaterPercent}></ha-svg-icon>
<span>
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_humidity",
humidity
@@ -1,3 +1,5 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiMinus, mdiPlus, mdiThermometer, mdiThermostat } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -23,8 +25,13 @@ import {
CLIMATE_HVAC_ACTION_TO_MODE,
ClimateEntityFeature,
} from "../../data/climate";
import {
apiContext,
configContext,
formattersContext,
internationalizationContext,
} from "../../data/context";
import { UNAVAILABLE } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import {
createStateControlCircularSliderController,
stateControlCircularSliderStyle,
@@ -44,10 +51,24 @@ const SLIDER_MODES: Record<HvacMode, ControlCircularSliderMode> = {
@customElement("ha-state-control-climate-temperature")
export class HaStateControlClimateTemperature extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ClimateEntity;
@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: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@property({ attribute: "show-secondary", type: Boolean })
public showSecondary = false;
@@ -77,7 +98,7 @@ export class HaStateControlClimateTemperature extends LitElement {
private get _step() {
return (
this.stateObj.attributes.target_temp_step ||
(this.hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
(this._config.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
);
}
@@ -119,14 +140,14 @@ export class HaStateControlClimateTemperature extends LitElement {
private _callService(type: string) {
if (type === "high" || type === "low") {
this.hass.callService("climate", "set_temperature", {
this._api.callService("climate", "set_temperature", {
entity_id: this.stateObj!.entity_id,
target_temp_low: this._targetTemperature.low,
target_temp_high: this._targetTemperature.high,
});
return;
}
this.hass.callService("climate", "set_temperature", {
this._api.callService("climate", "set_temperature", {
entity_id: this.stateObj!.entity_id,
temperature: this._targetTemperature.value,
});
@@ -164,7 +185,7 @@ export class HaStateControlClimateTemperature extends LitElement {
if (this.stateObj.state === UNAVAILABLE) {
return html`
<p class="label disabled">
${this.hass.formatEntityState(this.stateObj, UNAVAILABLE)}
${this._formatters.formatEntityState(this.stateObj, UNAVAILABLE)}
</p>
`;
}
@@ -181,9 +202,12 @@ export class HaStateControlClimateTemperature extends LitElement {
return html`
<p class="label">
${action && action !== "off"
? this.hass.formatEntityAttributeValue(this.stateObj, "hvac_action")
? this._formatters.formatEntityAttributeValue(
this.stateObj,
"hvac_action"
)
: isTemperatureDisplayed
? this.hass.formatEntityState(this.stateObj)
? this._formatters.formatEntityState(this.stateObj)
: nothing}
</p>
`;
@@ -237,14 +261,13 @@ export class HaStateControlClimateTemperature extends LitElement {
minimumFractionDigits: digits,
};
const unit = hideUnit ? "" : this.hass.config.unit_system.temperature;
const unit = hideUnit ? "" : this._config.config.unit_system.temperature;
if (style === "big") {
return html`
<ha-big-number
.value=${temperature}
.unit=${unit}
.hass=${this.hass}
.formatOptions=${formatOptions}
></ha-big-number>
`;
@@ -252,10 +275,10 @@ export class HaStateControlClimateTemperature extends LitElement {
const formatted = formatNumber(
temperature,
this.hass.locale,
this._i18n.locale,
formatOptions
);
return html`${formatted}${blankBeforeUnit(unit, this.hass.locale)}${unit}`;
return html`${formatted}${blankBeforeUnit(unit, this._i18n.locale)}${unit}`;
}
private _renderCurrent(temperature: number, style: "normal" | "big") {
@@ -266,15 +289,14 @@ export class HaStateControlClimateTemperature extends LitElement {
return html`
<ha-big-number
.value=${temperature}
.unit=${this.hass.config.unit_system.temperature}
.hass=${this.hass}
.unit=${this._config.config.unit_system.temperature}
.formatOptions=${formatOptions}
></ha-big-number>
`;
}
return html`
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_temperature",
temperature
@@ -321,7 +343,7 @@ export class HaStateControlClimateTemperature extends LitElement {
if (this.stateObj.state !== UNAVAILABLE) {
return html`
<p class="primary-state">
${this.hass.formatEntityState(this.stateObj)}
${this._formatters.formatEntityState(this.stateObj)}
</p>
`;
}
@@ -1,3 +1,5 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiMinus, mdiPlus, mdiThermostat, mdiWaterPercent } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
@@ -14,12 +16,12 @@ import "../../components/ha-outlined-icon-button";
import "../../components/ha-svg-icon";
import { UNAVAILABLE } from "../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../data/entity/entity_attributes";
import { apiContext, formattersContext } from "../../data/context";
import type { HumidifierEntity } from "../../data/humidifier";
import {
HUMIDIFIER_ACTION_MODE,
HumidifierEntityDeviceClass,
} from "../../data/humidifier";
import type { HomeAssistant } from "../../types";
import {
createStateControlCircularSliderController,
stateControlCircularSliderStyle,
@@ -27,10 +29,16 @@ import {
@customElement("ha-state-control-humidifier-humidity")
export class HaStateControlHumidifierHumidity extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HumidifierEntity;
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@property({ attribute: "show-secondary", type: Boolean })
public showSecondary = false;
@@ -79,7 +87,7 @@ export class HaStateControlHumidifierHumidity extends LitElement {
private _debouncedCallService = debounce(() => this._callService(), 1000);
private _callService() {
this.hass.callService("humidifier", "set_humidity", {
this._api.callService("humidifier", "set_humidity", {
entity_id: this.stateObj!.entity_id,
humidity: this._targetHumidity,
});
@@ -100,7 +108,7 @@ export class HaStateControlHumidifierHumidity extends LitElement {
if (this.stateObj.state === UNAVAILABLE) {
return html`
<p class="label disabled">
${this.hass.formatEntityState(this.stateObj, UNAVAILABLE)}
${this._formatters.formatEntityState(this.stateObj, UNAVAILABLE)}
</p>
`;
}
@@ -115,9 +123,9 @@ export class HaStateControlHumidifierHumidity extends LitElement {
return html`
<p class="label">
${action && action !== "off"
? this.hass.formatEntityAttributeValue(this.stateObj, "action")
? this._formatters.formatEntityAttributeValue(this.stateObj, "action")
: isHumidityDisplayed
? this.hass.formatEntityState(this.stateObj)
? this._formatters.formatEntityState(this.stateObj)
: nothing}
</p>
`;
@@ -156,7 +164,7 @@ export class HaStateControlHumidifierHumidity extends LitElement {
if (this.stateObj.state !== UNAVAILABLE) {
return html`
<p class="primary-state">
${this.hass.formatEntityState(this.stateObj)}
${this._formatters.formatEntityState(this.stateObj)}
</p>
`;
}
@@ -201,7 +209,6 @@ export class HaStateControlHumidifierHumidity extends LitElement {
<ha-big-number
.value=${humidity}
.unit=${DOMAIN_ATTRIBUTES_UNITS.humidifier.current_humidity}
.hass=${this.hass}
.formatOptions=${formatOptions}
unit-position="bottom"
></ha-big-number>
@@ -209,7 +216,7 @@ export class HaStateControlHumidifierHumidity extends LitElement {
}
return html`
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"humidity",
humidity
@@ -226,7 +233,6 @@ export class HaStateControlHumidifierHumidity extends LitElement {
<ha-big-number
.value=${humidity}
.unit=${DOMAIN_ATTRIBUTES_UNITS.humidifier.current_humidity}
.hass=${this.hass}
.formatOptions=${formatOptions}
unit-position="bottom"
></ha-big-number>
@@ -234,7 +240,7 @@ export class HaStateControlHumidifierHumidity extends LitElement {
}
return html`
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_humidity",
humidity
@@ -1,3 +1,5 @@
import { consume } from "@lit/context";
import type { ContextType } from "@lit/context";
import { mdiMinus, mdiPlus } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, html } from "lit";
@@ -5,10 +7,12 @@ import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../common/const";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color";
import { supportsFeature } from "../../common/entity/supports-feature";
import { clamp } from "../../common/number/clamp";
import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce";
import "../../components/ha-big-number";
import "../../components/ha-control-circular-slider";
@@ -17,7 +21,11 @@ import "../../components/ha-svg-icon";
import { UNAVAILABLE } from "../../data/entity/entity";
import type { WaterHeaterEntity } from "../../data/water_heater";
import { WaterHeaterEntityFeature } from "../../data/water_heater";
import type { HomeAssistant } from "../../types";
import {
apiContext,
configContext,
formattersContext,
} from "../../data/context";
import {
createStateControlCircularSliderController,
stateControlCircularSliderStyle,
@@ -25,10 +33,24 @@ import {
@customElement("ha-state-control-water_heater-temperature")
export class HaStateControlWaterHeaterTemperature extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: WaterHeaterEntity;
@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: formattersContext, subscribe: true })
private _formatters!: ContextType<typeof formattersContext>;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: "show-current", type: Boolean })
public showCurrent = false;
@@ -49,7 +71,7 @@ export class HaStateControlWaterHeaterTemperature extends LitElement {
private get _step() {
return (
this.stateObj.attributes.target_temp_step ||
(this.hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
(this._config.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
);
}
@@ -77,7 +99,7 @@ export class HaStateControlWaterHeaterTemperature extends LitElement {
private _debouncedCallService = debounce(() => this._callService(), 1000);
private _callService() {
this.hass.callService("water_heater", "set_temperature", {
this._api.callService("water_heater", "set_temperature", {
entity_id: this.stateObj!.entity_id,
temperature: this._targetTemperature,
});
@@ -98,7 +120,7 @@ export class HaStateControlWaterHeaterTemperature extends LitElement {
if (this.stateObj.state === UNAVAILABLE) {
return html`
<p class="label disabled">
${this.hass.formatEntityState(this.stateObj, UNAVAILABLE)}
${this._formatters.formatEntityState(this.stateObj, UNAVAILABLE)}
</p>
`;
}
@@ -111,12 +133,14 @@ export class HaStateControlWaterHeaterTemperature extends LitElement {
!this._targetTemperature
) {
return html`
<p class="label">${this.hass.formatEntityState(this.stateObj)}</p>
<p class="label">
${this._formatters.formatEntityState(this.stateObj)}
</p>
`;
}
return html`
<p class="label">${this.hass.localize("ui.card.water_heater.target")}</p>
<p class="label">${this._localize("ui.card.water_heater.target")}</p>
`;
}
@@ -148,8 +172,7 @@ export class HaStateControlWaterHeaterTemperature extends LitElement {
return html`
<ha-big-number
.value=${temperature}
.unit=${this.hass.config.unit_system.temperature}
.hass=${this.hass}
.unit=${this._config.config.unit_system.temperature}
.formatOptions=${formatOptions}
></ha-big-number>
`;
@@ -162,9 +185,9 @@ export class HaStateControlWaterHeaterTemperature extends LitElement {
return html`
<p class="label">
${this.hass.localize("ui.card.water_heater.currently")}
${this._localize("ui.card.water_heater.currently")}
<span>
${this.hass.formatEntityAttributeValue(
${this._formatters.formatEntityAttributeValue(
this.stateObj,
"current_temperature",
temperature
+91 -43
View File
@@ -1,3 +1,4 @@
import memoizeOne from "memoize-one";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
@@ -5,7 +6,10 @@ import { customElement, property } from "lit/decorators";
import { join } from "lit/directives/join";
import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { STRINGS_SEPARATOR_DOT } from "../common/const";
import {
STRINGS_SEPARATOR_DOT,
TIMESTAMP_STATE_DOMAINS,
} from "../common/const";
import "../components/ha-relative-time";
import { UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
import {
@@ -16,14 +20,7 @@ import type { UpdateEntity } from "../data/update";
import { computeUpdateStateDisplay } from "../data/update";
import "../panels/lovelace/components/hui-timestamp-display";
import type { HomeAssistant } from "../types";
const TIMESTAMP_STATE_DOMAINS = [
"button",
"infrared",
"input_button",
"radio_frequency",
"scene",
];
import { computeDomain } from "../common/entity/compute_domain";
export const STATE_DISPLAY_SPECIAL_CONTENT = [
"remaining_time",
@@ -60,6 +57,55 @@ export const DEFAULT_STATE_CONTENT_DOMAINS: Record<string, StateContent> = {
valve: ["state", "current_position"],
};
const TIMESTAMP_STATE_PROPS = ["last_updated", "last_changed"];
const TIMESTAMP_CONTENTS = [...TIMESTAMP_STATE_PROPS, "last_triggered"];
const TIMESTAMP_DOMAIN_CONTENTS = {
calendar: ["start_time", "end_time"],
input_datetime: ["timestamp"],
sun: [
"next_dawn",
"next_dusk",
"next_midnight",
"next_noon",
"next_rising",
"next_setting",
],
};
export const stateContentHasTimestamp = (
entityId?: string,
stateObj?: HassEntity,
content?: StateContent
): boolean => {
const contentArray = ensureArray(content);
if (content && contentArray.some((c) => TIMESTAMP_CONTENTS.includes(c))) {
return true;
}
if (!entityId) {
return false;
}
const domain = computeDomain(entityId);
if (!content || contentArray.includes("state")) {
if (TIMESTAMP_STATE_DOMAINS.includes(domain)) {
return true;
}
if (stateObj) {
const sensorDeviceClass =
domain === "sensor" ? stateObj.attributes.device_class : "";
if (SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(sensorDeviceClass)) {
return true;
}
}
}
return (
TIMESTAMP_DOMAIN_CONTENTS[domain] &&
content &&
contentArray.some((c) => TIMESTAMP_DOMAIN_CONTENTS[domain].includes(c))
);
};
@customElement("state-display")
class StateDisplay extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -70,6 +116,8 @@ class StateDisplay extends LitElement {
@property({ attribute: false }) public name?: string;
@property({ attribute: false }) public timeFormat?: string;
@property({ type: Boolean, attribute: "dash-unavailable" })
public dashUnavailable?: boolean;
@@ -77,9 +125,24 @@ class StateDisplay extends LitElement {
return this;
}
private _normalizeContent = memoizeOne(
(content?: StateContent): StateContent | undefined =>
content == null
? undefined
: ensureArray(content).map((s) => {
if (s === "last-updated") return "last_updated";
if (s === "last-changed") return "last_changed";
return s;
})
);
private get _content(): StateContent {
const domain = computeStateDomain(this.stateObj);
return this.content ?? DEFAULT_STATE_CONTENT_DOMAINS[domain] ?? "state";
return (
this._normalizeContent(this.content) ??
DEFAULT_STATE_CONTENT_DOMAINS[domain] ??
"state"
);
}
private _computeContent(
@@ -105,10 +168,11 @@ class StateDisplay extends LitElement {
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
.format=${this.stateObj.attributes.device_class ===
.format=${this.timeFormat ||
(this.stateObj.attributes.device_class ===
SENSOR_DEVICE_CLASS_UPTIME
? "total"
: "relative"}
: "relative")}
capitalize
></hui-timestamp-display>
`;
@@ -129,42 +193,26 @@ class StateDisplay extends LitElement {
return this.hass.formatEntityName(stateObj, { type }) || undefined;
}
let relativeDateTime: string | Date | undefined;
let relativeDateTime: string | number | undefined;
// Check last-changed for backwards compatibility
if (content === "last_changed" || content === "last-changed") {
relativeDateTime = stateObj.last_changed;
}
// Check last_updated for backwards compatibility
if (content === "last_updated" || content === "last-updated") {
relativeDateTime = stateObj.last_updated;
}
if (domain === "input_datetime" && content === "timestamp") {
relativeDateTime = new Date(stateObj.attributes.timestamp * 1000);
}
if (
content === "last_triggered" ||
(domain === "calendar" &&
(content === "start_time" || content === "end_time")) ||
(domain === "sun" &&
(content === "next_dawn" ||
content === "next_dusk" ||
content === "next_midnight" ||
content === "next_noon" ||
content === "next_rising" ||
content === "next_setting"))
if (TIMESTAMP_STATE_PROPS.includes(content)) {
relativeDateTime = stateObj[content];
} else if (domain === "input_datetime" && content === "timestamp") {
relativeDateTime = stateObj.attributes.timestamp * 1000;
} else if (
TIMESTAMP_CONTENTS.includes(content) ||
TIMESTAMP_DOMAIN_CONTENTS[domain]?.includes(content)
) {
relativeDateTime = stateObj.attributes[content];
}
if (relativeDateTime) {
return html`
<ha-relative-time
.datetime=${relativeDateTime}
capitalize
></ha-relative-time>
`;
if (relativeDateTime || relativeDateTime === 0) {
return html`<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(relativeDateTime)}
.format=${this.timeFormat}
capitalize
></hui-timestamp-display>`;
}
const specialContent = (STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain] ??
+16 -4
View File
@@ -466,6 +466,7 @@
"save": "Save",
"apply": "Apply",
"add": "Add",
"auto": "Auto",
"create": "Create",
"edit": "Edit",
"edit_item": "Edit {name}",
@@ -496,7 +497,8 @@
"replace": "Replace",
"append": "Append",
"supports_markdown": "Supports {markdown_help_link}",
"markdown": "Markdown"
"markdown": "Markdown",
"none": "None"
},
"components": {
"input": {
@@ -799,6 +801,15 @@
"floor": "Floor"
}
},
"time-format-picker": {
"formats": {
"relative": "Relative",
"total": "Total",
"datetime": "Date and time",
"time": "Time",
"date": "Date"
}
},
"subpage-data-table": {
"filters": "Filters",
"show_results": "Show {number} results",
@@ -2066,7 +2077,7 @@
"generic": {
"name": "Name",
"icon": "Icon",
"advanced_settings": "Advanced settings"
"more_options": "[%key:ui::dialogs::restart::more_options%]"
},
"input_datetime": {
"date": "Date",
@@ -5194,7 +5205,7 @@
"context_user_picked": "User firing event",
"context_user_pick": "Select user",
"description": {
"picker": "Triggers when an event is being received (event is an advanced concept in Home Assistant).",
"picker": "Triggers when an event is being received (events are a building block used across Home Assistant).",
"full": "When {eventTypes} event is fired"
}
},
@@ -5695,7 +5706,7 @@
"event": "[%key:ui::panel::config::automation::editor::triggers::type::event::event_type%]",
"event_data": "[%key:ui::panel::config::automation::editor::triggers::type::event::event_data%]",
"description": {
"picker": "Fires an event manually (event is an advanced concept in Home Assistant).",
"picker": "Fires an event manually (events are a building block used across Home Assistant).",
"full": "Manually fire event {name}",
"template": "based on a template"
}
@@ -9663,6 +9674,7 @@
"show_state": "Show state",
"show_last_changed": "Show last changed",
"tap_action": "Tap behavior",
"time_format": "Time format",
"interactions": "Interactions",
"title": "Title",
"theme": "Theme",
@@ -1,54 +0,0 @@
import { assert, describe, it } from "vitest";
import type { HassEntity } from "home-assistant-js-websocket";
import { featureClassNames } from "../../../src/common/entity/feature_class_names";
describe("featureClassNames", () => {
const classNames = {
1: "has-feature_a",
2: "has-feature_b",
4: "has-feature_c",
8: "has-feature_d",
};
it("Skips null states", () => {
const stateObj = null;
assert.strictEqual(featureClassNames(stateObj!, classNames), "");
});
it("Matches no features", () => {
// eslint-disable-next-line
const stateObj = <HassEntity>{
attributes: {
supported_features: 64,
},
};
assert.strictEqual(featureClassNames(stateObj, classNames), "");
});
it("Matches one feature", () => {
// eslint-disable-next-line
const stateObj = <HassEntity>{
attributes: {
supported_features: 72,
},
};
assert.strictEqual(
featureClassNames(stateObj, classNames),
"has-feature_d"
);
});
it("Matches two features", () => {
// eslint-disable-next-line
const stateObj = <HassEntity>{
attributes: {
supported_features: 73,
},
};
assert.strictEqual(
featureClassNames(stateObj, classNames),
"has-feature_a has-feature_d"
);
});
});