Compare commits

..

13 Commits

Author SHA1 Message Date
dependabot[bot] a1a15ae674 Bump actions/setup-node from 6.4.0 to 7.0.0 in /.github/actions/setup (#53200)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 10:46:56 +01:00
Simon Lamon b421a96eb0 Fix initial value rendering in dialog-move-datadisk.ts (#53193) 2026-07-20 09:45:50 +01:00
Simon Lamon 7dbfb2f9d2 Add CodeQL action to Dependabot groups (#53191) 2026-07-20 09:44:44 +01:00
Simon Lamon c68cd67cbd Add grouping for formatjs packages (#53192) 2026-07-20 09:44:24 +01:00
renovate[bot] 6de9841b6e Update formatjs monorepo (#53190)
* Update formatjs monorepo

* Update dependency @formatjs/intl-pluralrules to v6.3.13

* Update dependency @formatjs/intl-numberformat to v9.3.14

* Update dependency @formatjs/intl-durationformat to v0.10.18

* Update dependency @formatjs/intl-displaynames to v7.3.13

* Update dependency @formatjs/intl-datetimeformat to v7.5.1

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-19 08:06:09 +00:00
dependabot[bot] 35193447bf Bump github/codeql-action/init from 4.36.3 to 4.37.0 (#53186)
* Bump github/codeql-action/init from 4.36.3 to 4.37.0

Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.36.3 to 4.37.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...99df26d4f13ea111d4ec1a7dddef6063f76b97e9)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump github/codeql-action/analyze from 4.36.3 to 4.37.0

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.36.3 to 4.37.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/54f647b7e1bb85c95cddabcd46b0c578ec92bc1a...99df26d4f13ea111d4ec1a7dddef6063f76b97e9)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-19 09:42:43 +02:00
renovate[bot] 650444e92d Update dependency @lokalise/node-api to v16.2.0 (#53177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-19 09:13:40 +02:00
dependabot[bot] da9a78f752 Bump actions/stale from 10.3.0 to 10.4.0 (#53188)
Bumps [actions/stale](https://github.com/actions/stale) from 10.3.0 to 10.4.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899...1e223db275d687790206a7acac4d1a11bd6fe629)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-19 09:13:19 +02:00
dependabot[bot] 9077290b39 Bump actions/labeler from 6.1.0 to 6.2.0 (#53187)
Bumps [actions/labeler](https://github.com/actions/labeler) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/f27b608878404679385c85cfa523b85ccb86e213...b8dd2d9be0f68b860e7dae5dae7d772984eacd6d)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-19 09:12:59 +02:00
renovate[bot] e4c6ea29b3 Lock file maintenance (#53179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-19 09:12:40 +02:00
renovate[bot] aa57c27739 Update dependency @rsdoctor/rspack-plugin to v1.6.0 (#53174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-18 19:12:42 +02:00
Niklas Wagner 420137478e Add manufacturer, model, and model_id filtering to entity filter selector (#29636)
* Add manufacturer, model, and model_id filtering to entity selector

* Implement suggestion from review

* fix: simplify device data type

* Nest device filtering under `device` key in entity selector

* Fix race condition when fetching data

* Avoid config entries refetch loop on fetch failure

The catch handler reset _fetchedConfigEntries to false while also
assigning _configEntries, which re-rendered the component and re-armed
the fetch guard in updated() — an unbounded, no-backoff retry loop on
persistent failure. Keep the flag set so a failed fetch is not retried
on every re-render; the connection-change handler still retries on
reconnect.

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-07-18 09:51:15 +02:00
renovate[bot] 25bc81cf1f Update dependency @rspack/core to v2.1.4 (#53170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-17 17:38:02 +02:00
18 changed files with 1097 additions and 759 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ runs:
using: composite
steps:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: ".nvmrc"
cache: ${{ inputs.cache == 'true' && 'yarn' || '' }}
+4
View File
@@ -15,6 +15,10 @@ updates:
cooldown:
default-days: 7
open-pull-requests-limit: 10
groups:
codeql-action:
patterns:
- "github/codeql-action/*"
labels:
- Dependencies
- GitHub Actions
+2 -2
View File
@@ -32,12 +32,12 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
languages: javascript-typescript
build-mode: none
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
category: "/language:javascript-typescript"
+1 -1
View File
@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Apply labels
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
with:
sync-labels: true
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
+11 -11
View File
@@ -52,15 +52,15 @@
"@codemirror/view": "6.43.6",
"@date-fns/tz": "1.5.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.5.0",
"@formatjs/intl-displaynames": "7.3.12",
"@formatjs/intl-durationformat": "0.10.17",
"@formatjs/intl-datetimeformat": "7.5.1",
"@formatjs/intl-displaynames": "7.3.13",
"@formatjs/intl-durationformat": "0.10.18",
"@formatjs/intl-getcanonicallocales": "3.2.11",
"@formatjs/intl-listformat": "8.3.12",
"@formatjs/intl-listformat": "8.3.13",
"@formatjs/intl-locale": "5.3.10",
"@formatjs/intl-numberformat": "9.3.13",
"@formatjs/intl-pluralrules": "6.3.12",
"@formatjs/intl-relativetimeformat": "12.3.12",
"@formatjs/intl-numberformat": "9.3.14",
"@formatjs/intl-pluralrules": "6.3.13",
"@formatjs/intl-relativetimeformat": "12.3.13",
"@fullcalendar/core": "6.1.21",
"@fullcalendar/daygrid": "6.1.21",
"@fullcalendar/interaction": "6.1.21",
@@ -107,7 +107,7 @@
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.3.0",
"intl-messageformat": "11.2.11",
"intl-messageformat": "11.2.12",
"js-yaml": "5.2.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -146,13 +146,13 @@
"@bundle-stats/plugin-webpack-filter": "4.22.2",
"@eslint/js": "10.0.1",
"@html-eslint/eslint-plugin": "0.64.0",
"@lokalise/node-api": "16.1.0",
"@lokalise/node-api": "16.2.0",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.61.1",
"@rsdoctor/rspack-plugin": "1.5.18",
"@rspack/core": "2.1.3",
"@rsdoctor/rspack-plugin": "1.6.0",
"@rspack/core": "2.1.4",
"@rspack/dev-server": "2.1.0",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
+5
View File
@@ -86,6 +86,11 @@
"description": "Group date-fns with dependent timezone package",
"groupName": "date-fns",
"matchPackageNames": ["date-fns", "date-fns-tz"]
},
{
"description": "Group formatjs monorepo package",
"groupName": "formatjs",
"matchPackageNames": ["@formatjs/**"]
}
]
}
@@ -133,7 +133,13 @@ export class HaAreaSelector extends LitElement {
}
return ensureArray(this.selector.area.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices
)
);
};
@@ -147,7 +147,13 @@ export class HaDeviceSelector extends LitElement {
private _filterEntities = (entity: HassEntity): boolean =>
ensureArray(this.selector.device!.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices
)
);
}
@@ -2,8 +2,12 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { ConfigEntry } from "../../data/config_entries";
import { getConfigEntries } from "../../data/config_entries";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { EntitySelector } from "../../data/selector";
@@ -23,6 +27,8 @@ export class HaEntitySelector extends LitElement {
@state() private _entitySources?: EntitySources;
@state() private _configEntries?: ConfigEntry[];
@property() public value?: any;
@property() public label?: string;
@@ -37,12 +43,38 @@ export class HaEntitySelector extends LitElement {
@state() private _createDomains: string[] | undefined;
private _hasIntegration(selector: EntitySelector) {
return (
selector.entity?.filter &&
ensureArray(selector.entity.filter).some((filter) => filter.integration)
);
}
private _deviceIntegrationLookup = memoizeOne(
(
entitySources: EntitySources,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
configEntries?: ConfigEntry[]
) =>
getDeviceIntegrationLookup(
entitySources,
Object.values(entities),
Object.values(devices),
configEntries
)
);
// Which async data the current filter needs to be evaluated: a top-level or
// device `integration` filter needs entity sources, and a `device.integration`
// filter additionally needs config entries (the device integration lookup is
// built from both).
private _dataNeeds = memoizeOne((selector: EntitySelector) => {
const filters = selector.entity?.filter
? ensureArray(selector.entity.filter)
: [];
return {
entitySources: filters.some(
(f) => f.integration || f.device?.integration
),
configEntries: filters.some((f) => f.device?.integration),
};
});
private _fetchedConfigEntries = false;
protected willUpdate(changedProperties: PropertyValues<this>): void {
if (changedProperties.get("selector") && this.value !== undefined) {
@@ -57,7 +89,11 @@ export class HaEntitySelector extends LitElement {
}
protected render() {
if (this._hasIntegration(this.selector) && !this._entitySources) {
const needs = this._dataNeeds(this.selector);
if (
(needs.entitySources && !this._entitySources) ||
(needs.configEntries && !this._configEntries)
) {
return nothing;
}
@@ -96,15 +132,37 @@ export class HaEntitySelector extends LitElement {
protected updated(changedProps: PropertyValues<this>): void {
super.updated(changedProps);
if (
changedProps.has("selector") &&
this._hasIntegration(this.selector) &&
!this._entitySources
) {
// The connection changed (e.g. reconnect); refetch config entries.
const oldHass = changedProps.get("hass");
if (oldHass && oldHass.connection !== this.hass.connection) {
this._fetchedConfigEntries = false;
this._configEntries = undefined;
}
const needs = this._dataNeeds(this.selector);
if (needs.entitySources && !this._entitySources) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
});
}
if (needs.configEntries && !this._fetchedConfigEntries) {
this._fetchedConfigEntries = true;
getConfigEntries(this.hass)
.then((entries) => {
this._configEntries = entries;
})
.catch(() => {
// Fall back to no entries so the picker still renders. We keep
// `_fetchedConfigEntries` set so the failed fetch is not retried on
// every re-render; the connection-change handler above retries on
// reconnect.
this._configEntries = [];
});
}
if (changedProps.has("selector")) {
this._createDomains = computeCreateDomains(this.selector);
}
@@ -114,8 +172,25 @@ export class HaEntitySelector extends LitElement {
if (!this.selector?.entity?.filter) {
return true;
}
const deviceIntegrationLookup =
this._entitySources && this._dataNeeds(this.selector).configEntries
? this._deviceIntegrationLookup(
this._entitySources,
this.hass.entities,
this.hass.devices,
this._configEntries
)
: undefined;
return ensureArray(this.selector.entity.filter).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices,
deviceIntegrationLookup
)
);
};
}
@@ -133,7 +133,13 @@ export class HaFloorSelector extends LitElement {
}
return ensureArray(this.selector.floor.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices
)
);
};
@@ -93,7 +93,13 @@ export class HaTargetSelector extends LitElement {
}
return ensureArray(this.selector.target.entity).some((filter) =>
filterSelectorEntities(filter, entity, this._entitySources)
filterSelectorEntities(
filter,
entity,
this._entitySources,
this.hass.entities,
this.hass.devices
)
);
};
+7 -1
View File
@@ -895,7 +895,13 @@ export class HaServiceControl extends LitElement {
}
if (targetEntities.length) {
targetEntities = targetEntities.filter((entity) =>
entityMeetsTargetSelector(this.hass.states[entity], targetSelector)
entityMeetsTargetSelector(
this.hass.states[entity],
targetSelector,
undefined,
this.hass.entities,
this.hass.devices
)
);
}
target = {
+53 -10
View File
@@ -266,6 +266,10 @@ interface EntitySelectorFilter {
unit_of_measurement?: string | readonly string[];
}
interface EntitySelectorEntityFilter extends EntitySelectorFilter {
device?: DeviceSelectorFilter;
}
export interface EntitySelectorExtraOption {
id: string;
primary: string;
@@ -281,7 +285,7 @@ export interface EntitySelector {
multiple?: boolean;
include_entities?: string[];
exclude_entities?: string[];
filter?: EntitySelectorFilter | readonly EntitySelectorFilter[];
filter?: EntitySelectorEntityFilter | readonly EntitySelectorEntityFilter[];
reorder?: boolean;
extra_options?: EntitySelectorExtraOption[];
} | null;
@@ -671,7 +675,9 @@ export const expandLabelTarget = (
entityMeetsTargetSelector(
hass.states[entity.entity_id],
targetSelector,
entitySources
entitySources,
hass.entities,
hass.devices
)
) {
newEntities.push(entity.entity_id);
@@ -737,7 +743,9 @@ export const expandAreaTarget = (
entityMeetsTargetSelector(
hass.states[entity.entity_id],
targetSelector,
entitySources
entitySources,
hass.entities,
hass.devices
)
) {
newEntities.push(entity.entity_id);
@@ -760,7 +768,9 @@ export const expandDeviceTarget = (
entityMeetsTargetSelector(
hass.states[entity.entity_id],
targetSelector,
entitySources
entitySources,
hass.entities,
hass.devices
)
) {
newEntities.push(entity.entity_id);
@@ -801,7 +811,9 @@ export const areaMeetsTargetSelector = (
entityMeetsTargetSelector(
hass.states[entity.entity_id],
targetSelector,
entitySources
entitySources,
hass.entities,
hass.devices
)
) {
return true;
@@ -849,14 +861,22 @@ export const deviceMeetsTargetSelector = (
export const entityMeetsTargetSelector = (
entity: HassEntity | undefined,
targetSelector: TargetSelector,
entitySources?: EntitySources
entitySources?: EntitySources,
entities?: HomeAssistant["entities"],
devices?: HomeAssistant["devices"]
): boolean => {
if (!entity) {
return false;
}
if (targetSelector.target?.entity) {
return ensureArray(targetSelector.target!.entity).some((filterEntity) =>
filterSelectorEntities(filterEntity, entity, entitySources)
filterSelectorEntities(
filterEntity,
entity,
entitySources,
entities,
devices
)
);
}
return true;
@@ -895,9 +915,12 @@ export const filterSelectorDevices = (
};
export const filterSelectorEntities = (
filterEntity: EntitySelectorFilter,
filterEntity: EntitySelectorEntityFilter,
entity: HassEntity,
entitySources?: EntitySources
entitySources?: EntitySources,
entityRegistry?: HomeAssistant["entities"],
devices?: HomeAssistant["devices"],
deviceIntegrationLookup?: Record<string, Set<string>>
): boolean => {
const {
domain: filterDomain,
@@ -905,6 +928,7 @@ export const filterSelectorEntities = (
supported_features: filterSupportedFeature,
unit_of_measurement: filterUnitOfMeasurement,
integration: filterIntegration,
device: filterDevice,
} = filterEntity;
if (filterDomain) {
@@ -951,6 +975,24 @@ export const filterSelectorEntities = (
}
}
if (filterDevice) {
if (!entityRegistry || !devices) {
return false;
}
const deviceId = entityRegistry[entity.entity_id]?.device_id;
if (!deviceId) {
return false;
}
const device = devices[deviceId];
if (!device) {
return false;
}
if (!filterSelectorDevices(filterDevice, device, deviceIntegrationLookup)) {
return false;
}
}
if (
filterIntegration &&
entitySources?.[entity.entity_id]?.domain !== filterIntegration
@@ -1020,7 +1062,7 @@ export const handleLegacyDeviceSelector = (
export const computeCreateDomains = (
selector: EntitySelector | TargetSelector
): undefined | string[] => {
let entityFilters: EntitySelectorFilter[] | undefined;
let entityFilters: EntitySelectorEntityFilter[] | undefined;
if ("target" in selector) {
entityFilters = ensureArray(selector.target?.entity);
@@ -1038,6 +1080,7 @@ export const computeCreateDomains = (
!entityFilter.integration &&
!entityFilter.device_class &&
!entityFilter.supported_features &&
!entityFilter.device &&
entityFilter.domain
? ensureArray(entityFilter.domain).filter((domain) =>
isHelperDomain(domain)
@@ -156,6 +156,7 @@ class MoveDatadiskDialog extends DirtyStateProviderMixin<MoveDatadiskFormState>(
.label=${this.hass.localize(
"ui.panel.config.storage.datadisk.select_device"
)}
.value=${this._selectedDevice}
@selected=${this._selectDevice}
.options=${this._disks.map((disk) => ({
value: disk.id,
@@ -1,5 +1,5 @@
import { html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
array,
@@ -12,15 +12,14 @@ import {
optional,
string,
} from "superstruct";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { ValueChangedEvent } from "../../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { ClockCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
@@ -65,13 +64,17 @@ export class HuiClockCardEditor
extends LitElement
implements LovelaceCardEditor
{
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: ClockCardConfig;
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
(
localize: LocalizeFunc,
clockStyle: ClockCardConfig["clock_style"],
ticks: ClockCardConfig["ticks"],
showSeconds: boolean | undefined
) =>
[
{ name: "title", selector: { text: {} } },
{
@@ -111,122 +114,124 @@ export class HuiClockCardEditor
ui_clock_date_format: {},
},
},
{
name: "time_format",
hidden: {
field: "clock_style",
operator: "not_eq",
value: "digital",
},
selector: {
select: {
mode: "dropdown",
options: ["auto", ...Object.values(TimeFormat)].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.time_formats.${value}`
),
})),
},
},
},
{
name: "border",
hidden: { field: "clock_style", operator: "not_eq", value: "analog" },
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.border.description`
),
},
default: false,
selector: {
boolean: {},
},
},
{
name: "ticks",
hidden: { field: "clock_style", operator: "not_eq", value: "analog" },
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.ticks.description`
),
},
default: "hour",
selector: {
select: {
mode: "dropdown",
options: ["none", "quarter", "hour", "minute"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.ticks.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.ticks.${value}.description`
),
})),
},
},
},
{
name: "seconds_motion",
hidden: {
condition: "or",
conditions: [
{ field: "clock_style", operator: "not_eq", value: "analog" },
{ field: "show_seconds", operator: "not_eq", value: true },
],
},
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
),
},
default: "continuous",
selector: {
select: {
mode: "dropdown",
options: ["continuous", "tick"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.description`
),
})),
},
},
},
{
name: "face_style",
hidden: {
condition: "or",
conditions: [
{ field: "clock_style", operator: "not_eq", value: "analog" },
{ field: "ticks", value: "none" },
],
},
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.face_style.description`
),
},
default: "markers",
selector: {
select: {
mode: "dropdown",
options: ["markers", "numbers_upright", "roman"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.face_style.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.face_style.${value}.description`
),
})),
},
},
},
...(clockStyle === "digital"
? ([
{
name: "time_format",
selector: {
select: {
mode: "dropdown",
options: ["auto", ...Object.values(TimeFormat)].map(
(value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.time_formats.${value}`
),
})
),
},
},
},
] as const satisfies readonly HaFormSchema[])
: clockStyle === "analog"
? ([
{
name: "border",
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.border.description`
),
},
default: false,
selector: {
boolean: {},
},
},
{
name: "ticks",
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.ticks.description`
),
},
default: "hour",
selector: {
select: {
mode: "dropdown",
options: ["none", "quarter", "hour", "minute"].map(
(value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.ticks.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.ticks.${value}.description`
),
})
),
},
},
},
...(showSeconds
? ([
{
name: "seconds_motion",
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
),
},
default: "continuous",
selector: {
select: {
mode: "dropdown",
options: ["continuous", "tick"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.description`
),
})),
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
...(ticks !== "none"
? ([
{
name: "face_style",
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.face_style.description`
),
},
default: "markers",
selector: {
select: {
mode: "dropdown",
options: [
"markers",
"numbers_upright",
"roman",
].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.face_style.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.face_style.${value}.description`
),
})),
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[])
: []),
{ name: "time_zone", selector: { timezone: {} } },
] as const satisfies readonly HaFormSchema[]
);
@@ -260,14 +265,20 @@ export class HuiClockCardEditor
}
protected render() {
if (!this._config) {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._data(this._config)}
.schema=${this._schema(this._localize)}
.schema=${this._schema(
this.hass.localize,
this._data(this._config).clock_style,
this._data(this._config).ticks,
this._data(this._config).show_seconds
)}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
@@ -316,43 +327,51 @@ export class HuiClockCardEditor
) => {
switch (schema.name) {
case "title":
return this._localize("ui.panel.lovelace.editor.card.generic.title");
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.title"
);
case "clock_style":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.clock_style`
);
case "clock_size":
return this._localize(`ui.panel.lovelace.editor.card.clock.clock_size`);
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.clock_size`
);
case "time_format":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.time_format`
);
case "time_zone":
return this._localize(`ui.panel.lovelace.editor.card.clock.time_zone`);
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.time_zone`
);
case "show_seconds":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.show_seconds`
);
case "no_background":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.no_background`
);
case "date_format":
return this._localize(`ui.panel.lovelace.editor.card.clock.date.label`);
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.date.label`
);
case "border":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.border.label`
);
case "ticks":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.ticks.label`
);
case "seconds_motion":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.label`
);
case "face_style":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.face_style.label`
);
default:
@@ -365,23 +384,23 @@ export class HuiClockCardEditor
) => {
switch (schema.name) {
case "date_format":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.date.description`
);
case "border":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.border.description`
);
case "ticks":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.ticks.description`
);
case "seconds_motion":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
);
case "face_style":
return this._localize(
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.face_style.description`
);
default:
+134
View File
@@ -0,0 +1,134 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { describe, expect, it } from "vitest";
import type { DeviceRegistryEntry } from "../../src/data/device/device_registry";
import { filterSelectorEntities } from "../../src/data/selector";
import type { HomeAssistant } from "../../src/types";
const entity = {
entity_id: "light.living_room",
state: "on",
attributes: {},
} as HassEntity;
const entityRegistry = {
"light.living_room": { device_id: "device_1" },
} as unknown as HomeAssistant["entities"];
const devices = {
device_1: {
id: "device_1",
manufacturer: "Signify",
model: "Hue Bulb",
model_id: "LCT015",
} as DeviceRegistryEntry,
} as unknown as HomeAssistant["devices"];
describe("filterSelectorEntities device filter", () => {
it("matches when the nested device manufacturer matches", () => {
expect(
filterSelectorEntities(
{ device: { manufacturer: "Signify" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(true);
});
it("does not match when the nested device manufacturer differs", () => {
expect(
filterSelectorEntities(
{ device: { manufacturer: "Sonos" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(false);
});
it("matches when model and model_id both match", () => {
expect(
filterSelectorEntities(
{ device: { model: "Hue Bulb", model_id: "LCT015" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(true);
});
it("does not match when one of model or model_id differs", () => {
expect(
filterSelectorEntities(
{ device: { model: "Hue Bulb", model_id: "OTHER" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(false);
});
it("matches the device integration via the lookup", () => {
expect(
filterSelectorEntities(
{ device: { integration: "hue" } },
entity,
undefined,
entityRegistry,
devices,
{ device_1: new Set(["hue"]) }
)
).toBe(true);
});
it("does not match a device integration that is absent from the lookup", () => {
expect(
filterSelectorEntities(
{ device: { integration: "zha" } },
entity,
undefined,
entityRegistry,
devices,
{ device_1: new Set(["hue"]) }
)
).toBe(false);
});
it("does not match when the entity has no underlying device", () => {
expect(
filterSelectorEntities(
{ device: { manufacturer: "Signify" } },
entity,
undefined,
{} as HomeAssistant["entities"],
devices
)
).toBe(false);
});
it("combines device conditions with other entity conditions (AND)", () => {
expect(
filterSelectorEntities(
{ domain: "light", device: { manufacturer: "Signify" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(true);
expect(
filterSelectorEntities(
{ domain: "switch", device: { manufacturer: "Signify" } },
entity,
undefined,
entityRegistry,
devices
)
).toBe(false);
});
});
+600 -573
View File
File diff suppressed because it is too large Load Diff