Merge pull request #66 from bramkragten/ha-entity-names

Resolve entity names via hass.formatEntityName
This commit is contained in:
2026-09-15 14:47:20 +03:00
committed by GitHub
5 changed files with 103 additions and 9 deletions
+25 -1
View File
@@ -111,7 +111,7 @@ layout:
| ----------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
| type | string | **Required** | `custom:time-picker-card` | |
| entity | string | **Required** | [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) entity with `has_time: true` | |
| name | string | **Optional** | Card name | Entity's `friendly_name` |
| name | string / list | **Optional** | Card name. Accepts a [structured name](#structured-names) on Home Assistant 2026.4 and later. | Entity's name |
| link_values | boolean | **Optional** | If enabled, will change hour when minutes overflow. E.g. will go from 11:55 to 12:00, instead of 11:00 | `false` |
| hour_mode | `12` or `24` | **Optional** | Hour format. If `12`, card will show AM/PM picker | `24` |
| hour_step | number | **Optional** | Hour change when clicking arrows | `1` |
@@ -156,6 +156,30 @@ you can use the following variables in your theme's config file:
| time-picker-border-radius | `var(--ha-card-border-radius)` | Border radius of the card |
| time-picker-control-padding | `8px` | Padding for interactive elements. Increase for larger hitboxes of the controls. Example: `10px 12px` |
### Structured names
*Requires Home Assistant 2026.4 or later. On earlier versions a structured `name` falls back to the entity's friendly name.*
Home Assistant composes an entity's display name out of its registry context
(entity, device, area, floor) rather than one `friendly_name` string. `name` can
be a list of those parts instead of a plain string, so it keeps following renames
and matches what the built-in cards show:
```yaml
type: custom:time-picker-card
entity: input_datetime.alarm
name:
- type: area
- type: entity
```
Available part types are `entity`, `device`, `parent_device`, `area`, `floor`, and
`text` (a literal, written as `{type: text, text: 'Alarm'}`). Parts that resolve to
nothing are dropped. A plain string `name` keeps working exactly as before, and the
visual editor offers both modes on Home Assistant 2025.11 and later.
See the [Home Assistant developer documentation](https://developers.home-assistant.io/docs/frontend/data#hassformatentitynamestateobj-name-options) for details.
## Meta
**Georgi Gardev**
+13 -6
View File
@@ -1,4 +1,5 @@
import { HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers';
import { supportsEntityNameSelector } from './entity-name';
import { html, LitElement, TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { TimePickerCardConfig } from './types';
@@ -17,12 +18,18 @@ const NAME_TO_LABEL_MAP = {
seconds: 'Seconds',
};
const SCHEMA = [
type FormSchemaEntry = Record<string, unknown>;
// The entity_name selector, which lets users compose a name out of registry
// parts, was added in HA 2025.11. Older versions fall back to a plain text field.
const nameSchema = (hass: HomeAssistant): FormSchemaEntry =>
supportsEntityNameSelector(hass)
? { name: 'name', selector: { entity_name: {} }, context: { entity: 'entity' } }
: { name: 'name', selector: { text: {} } };
const schema = (hass: HomeAssistant): FormSchemaEntry[] => [
{ name: 'entity', selector: { entity: { domain: 'input_datetime' } } },
{
name: 'name',
selector: { text: {} },
},
nameSchema(hass),
{
type: 'grid',
schema: [
@@ -135,7 +142,7 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
<ha-form
.hass=${this.hass}
.data=${this.config}
.schema=${SCHEMA}
.schema=${schema(this.hass)}
.computeLabel=${this.computeLabel}
@value-changed=${this.valueChanged}
></ha-form>
+55
View File
@@ -0,0 +1,55 @@
import { HomeAssistant } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import { EntityName } from './types';
// hass.formatEntityName only accepts a card's `name` option (a user string, a
// structured name, or undefined) from HA 2026.4. Earlier versions expose the
// same helper with an incompatible signature, so feature detection is not
// enough - the version has to be checked.
function atLeastVersion(hass: HomeAssistant, major: number, minor: number): boolean {
const [haMajor, haMinor] = (hass.config?.version ?? '').split('.', 2);
return Number(haMajor) > major || (Number(haMajor) === major && Number(haMinor) >= minor);
}
function supportsEntityNames(hass: HomeAssistant): boolean {
// A hass can report a recent version without carrying the helper (a test
// harness, or a hass that has not finished initialising), and calling it
// then throws - so the version gate alone is not enough.
if (!hass || typeof (hass as { formatEntityName?: unknown }).formatEntityName !== 'function') {
return false;
}
return atLeastVersion(hass, 2026, 4);
}
/**
* The `entity_name` selector, which lets users compose a name out of registry
* parts in the visual editor, was added in HA 2025.11.
*/
export function supportsEntityNameSelector(hass: HomeAssistant): boolean {
return atLeastVersion(hass, 2025, 11);
}
type HassWithEntityNames = HomeAssistant & {
formatEntityName: (stateObj: HassEntity, name: EntityName | undefined) => string;
};
/**
* Resolves a `name` option against the entity's registry context (entity,
* device, area, floor). Falls back to the friendly name on Home Assistant
* versions that cannot resolve a structured name.
*/
export function computeEntityName(
hass: HomeAssistant,
stateObj: HassEntity | undefined,
name: EntityName | undefined
): string | undefined {
const configuredName = typeof name === 'string' ? name : undefined;
if (!stateObj) {
return configuredName;
}
if (supportsEntityNames(hass)) {
return (hass as HassWithEntityNames).formatEntityName(stateObj, name);
}
return configuredName ?? stateObj.attributes.friendly_name;
}
+2 -1
View File
@@ -11,6 +11,7 @@ import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
import { actionHandler } from './action-handler-directive';
import { computeEntityName } from './entity-name';
import './components/time-period.component';
import './components/time-unit.component';
import {
@@ -79,7 +80,7 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
}
private get name(): string | undefined {
return this.config.name || this.entity?.attributes.friendly_name;
return computeEntityName(this.hass, this.entity, this.config.name);
}
private get shouldShowPeriod(): boolean {
+8 -1
View File
@@ -2,7 +2,7 @@ import { ActionConfig, LovelaceCardConfig } from 'custom-card-helpers';
export interface TimePickerCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
name?: EntityName;
link_values?: boolean;
hour_mode?: HourMode;
hour_step?: number;
@@ -16,6 +16,13 @@ export interface TimePickerCardConfig extends LovelaceCardConfig {
hold_action?: ActionConfig;
}
/** A `name` option: a plain string, or name parts resolved from the registry. */
export type EntityName = string | EntityNameItem | EntityNameItem[];
export type EntityNameItem =
| { type: 'entity' | 'device' | 'parent_device' | 'area' | 'floor' }
| { type: 'text'; text: string };
export type HourMode = 12 | 24 | undefined;
export interface TimePickerLayoutConfig {