Compare commits

...

5 Commits

Author SHA1 Message Date
Petar Petrov d7c15dd194 Restore per-second ticking for ha-relative-time 2026-07-09 14:27:17 +03:00
AlCalzone 7e244a706b Z-Wave: create users with credentials in a single API call (#52339)
* Z-Wave: use add_user service to add users with credentials

* refactor to match backend changes
2026-07-09 06:27:25 -04:00
renovate[bot] 7911aef6c0 Update vitest monorepo to v4.1.10 (#53067)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-09 11:44:12 +03:00
renovate[bot] ed00d4ded1 Update dependency @codemirror/view to v6.43.6 (#53066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-09 11:43:36 +03:00
Petar Petrov be55a2197e Wrap device automation picker option labels instead of truncating (#53065) 2026-07-09 10:30:28 +02:00
8 changed files with 179 additions and 125 deletions
+3 -3
View File
@@ -49,7 +49,7 @@
"@codemirror/lint": "6.9.7",
"@codemirror/search": "6.7.1",
"@codemirror/state": "6.7.1",
"@codemirror/view": "6.43.5",
"@codemirror/view": "6.43.6",
"@date-fns/tz": "1.5.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.4.9",
@@ -168,7 +168,7 @@
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@vitest/coverage-v8": "4.1.9",
"@vitest/coverage-v8": "4.1.10",
"babel-loader": "10.1.1",
"babel-plugin-polyfill-corejs3": "1.0.0",
"browserslist-useragent-regexp": "4.1.4",
@@ -212,7 +212,7 @@
"typescript": "6.0.3",
"typescript-eslint": "8.62.1",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.9",
"vitest": "4.1.10",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
@@ -1,3 +1,4 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { consume } from "@lit/context";
import type { HassEntities } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
@@ -15,7 +16,12 @@ import {
} from "../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { CallWS, HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box-item";
import "../ha-generic-picker";
import {
DEFAULT_ROW_RENDERER_CONTENT,
type PickerComboBoxItem,
} from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
@@ -105,6 +111,7 @@ export abstract class HaDeviceAutomationPicker<
.disabled=${!this._automations || this._automations.length === 0}
.getItems=${this._getItems(value, this._automations)}
@value-changed=${this._automationChanged}
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
.unknownItemText=${this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
@@ -160,6 +167,13 @@ export abstract class HaDeviceAutomationPicker<
}
);
// Device automation labels (entity name + subtype) are often longer than the
// field, so let the option wrap onto multiple lines instead of truncating.
private _rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) =>
html`<ha-combo-box-item type="button" compact multiline>
${DEFAULT_ROW_RENDERER_CONTENT(item)}
</ha-combo-box-item>`;
private _valueRenderer: PickerValueRenderer = (value: string) => {
const automation = this._automations?.find(
(a, idx) => value === `${a.device_id}_${idx}`
+9
View File
@@ -7,6 +7,11 @@ export class HaComboBoxItem extends HaMdListItem {
@property({ type: Boolean, reflect: true, attribute: "border-top" })
public borderTop = false;
// Allow the headline/supporting text to wrap onto multiple lines instead of
// truncating with an ellipsis. Off by default to preserve single-line rows.
@property({ type: Boolean, reflect: true })
public multiline = false;
static override styles = [
...haMdListStyles,
css`
@@ -41,6 +46,10 @@ export class HaComboBoxItem extends HaMdListItem {
font-size: var(--ha-font-size-s);
white-space: nowrap;
}
:host([multiline]) [slot="headline"],
:host([multiline]) [slot="supporting-text"] {
white-space: normal;
}
::slotted(state-badge),
::slotted(img) {
width: 32px;
+35 -39
View File
@@ -20,18 +20,16 @@ class HaRelativeTime extends ReactiveElement {
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: HomeAssistantInternationalization;
private _interval?: number;
private _timeout?: number;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._clearInterval();
this._clearTimeout();
}
public connectedCallback(): void {
super.connectedCallback();
if (this.datetime) {
this._startInterval();
}
this._updateRelative();
}
protected createRenderRoot() {
@@ -40,31 +38,19 @@ class HaRelativeTime extends ReactiveElement {
protected update(changedProps: PropertyValues<this>) {
super.update(changedProps);
if (changedProps.has("datetime")) {
if (this.datetime) {
this._startInterval();
} else {
this._clearInterval();
}
}
this._updateRelative();
}
private _clearInterval(): void {
if (this._interval) {
window.clearInterval(this._interval);
this._interval = undefined;
private _clearTimeout(): void {
if (this._timeout) {
window.clearTimeout(this._timeout);
this._timeout = undefined;
}
}
private _startInterval(): void {
this._clearInterval();
// update every 60 seconds
this._interval = window.setInterval(() => this._updateRelative(), 60000);
}
private _updateRelative(): void {
this._clearTimeout();
if (!this._i18n) {
return;
}
@@ -73,23 +59,33 @@ class HaRelativeTime extends ReactiveElement {
this.textContent = this._i18n.localize(
"ui.components.relative_time.never"
);
} else {
const date =
typeof this.datetime === "string"
? parseISO(this.datetime)
: this.datetime;
const relTime = relativeTime(
date,
this._i18n.locale,
undefined,
true,
this.format
);
this.textContent = this.capitalize
? capitalizeFirstLetter(relTime)
: relTime;
return;
}
const date =
typeof this.datetime === "string"
? parseISO(this.datetime)
: this.datetime;
const relTime = relativeTime(
date,
this._i18n.locale,
undefined,
true,
this.format
);
this.textContent = this.capitalize
? capitalizeFirstLetter(relTime)
: relTime;
// Keep the relative time counting up on its own. Refresh every second
// while the difference is still measured in seconds, otherwise every
// minute.
const secondsDiff = Math.abs(Date.now() - date.getTime()) / 1000;
this._timeout = window.setTimeout(
() => this._updateRelative(),
secondsDiff < 60 ? 1000 : 60000
);
}
}
+4
View File
@@ -117,10 +117,14 @@ export interface SetZwaveUserParams {
user_type?: string;
credential_rule?: string;
active?: boolean;
credential_type?: ZwaveCredentialType;
credential_slot?: number;
credential_data?: string;
}
export interface SetZwaveUserResult {
user_id: number;
credential_slot?: number | null;
}
export interface SetZwaveCredentialParams {
@@ -19,7 +19,6 @@ import {
DEFAULT_CREDENTIAL_MIN_LENGTH,
ENTERABLE_ZWAVE_CREDENTIAL_TYPES,
deleteZwaveCredential,
deleteZwaveUser,
enterableCredentialTypes,
getCredentialError,
compatibleUserTypes,
@@ -539,27 +538,16 @@ class DialogZwaveCredentialUserEdit extends DirtyStateProviderMixin<CredentialFo
credentialType: ZwaveCredentialType
): Promise<void> {
const params = this._params!;
const { user_id } = await setZwaveUser(this.hass, params.entity_id, {
user_name: this._supportsUserNames ? this._userName.trim() : undefined,
user_type: this._userType,
active: true,
});
try {
await setZwaveCredential(this.hass, params.entity_id, {
user_id,
await setZwaveUser(this.hass, params.entity_id, {
// Omit user_id to create a new user with the given credential.
user_name: this._supportsUserNames ? this._userName.trim() : undefined,
user_type: this._userType,
active: true,
credential_type: credentialType,
credential_data: this._credentialData,
});
} catch (err: unknown) {
// Roll back the user so the lock returns to its prior state. We
// ignore rollback errors — the credential error is the actionable
// one to surface; a stranded user will reappear on next refresh.
try {
await deleteZwaveUser(this.hass, params.entity_id, user_id);
} catch {
// Ignore.
}
this._error = this.hass.localize(
"ui.panel.config.zwave_js.credentials.errors.add_user_failed",
{
+43
View File
@@ -169,6 +169,49 @@ describe("zwave_js-credentials", () => {
);
expect(result.user_id).toBe(1);
});
it("creates a new user with a credential and returns the slot", async () => {
// Omitting user_id makes set_user create a new user and write the
// credential in one call, returning the allocated credential_slot.
const hass = setUserResponse({ user_id: 1, credential_slot: 1 });
const result = await setZwaveUser(hass, ENTITY_ID, {
user_name: "Alice",
user_type: "general",
active: true,
credential_type: "pin_code",
credential_data: "1234",
});
expect(hass.callService).toHaveBeenCalledWith(
"zwave_js",
"set_user",
{
user_name: "Alice",
user_type: "general",
active: true,
credential_type: "pin_code",
credential_data: "1234",
},
{ entity_id: ENTITY_ID },
false,
true
);
expect(result).toEqual({ user_id: 1, credential_slot: 1 });
});
it("propagates errors from callService", async () => {
const hass = {
callService: vi.fn().mockRejectedValue(new Error("No slots")),
} as unknown as HomeAssistant;
await expect(
setZwaveUser(hass, ENTITY_ID, {
credential_type: "pin_code",
credential_data: "1234",
})
).rejects.toThrow("No slots");
});
});
describe("deleteZwaveUser", () => {
+66 -66
View File
@@ -2454,15 +2454,15 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/view@npm:6.43.5, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
version: 6.43.5
resolution: "@codemirror/view@npm:6.43.5"
"@codemirror/view@npm:6.43.6, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
version: 6.43.6
resolution: "@codemirror/view@npm:6.43.6"
dependencies:
"@codemirror/state": "npm:^6.7.0"
crelt: "npm:^1.0.6"
style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4"
checksum: 10/24e28f57d84d1db5c140f704c1af8591c279099d26f864a12a16cdd8a372a10048327ea81d475da57d0e84ab9ac49ad71209d54aa1211dd81d79a144e5b6039c
checksum: 10/be058e86523d5770921c6bad7963eb5ef0a7db340922db93043394e1e93d1f3125611a3c2dbf143f1ab69b426038cd2034ffa7484d4b55d1ac909f2ceed2ee06
languageName: node
linkType: hard
@@ -6142,12 +6142,12 @@ __metadata:
languageName: node
linkType: hard
"@vitest/coverage-v8@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/coverage-v8@npm:4.1.9"
"@vitest/coverage-v8@npm:4.1.10":
version: 4.1.10
resolution: "@vitest/coverage-v8@npm:4.1.10"
dependencies:
"@bcoe/v8-coverage": "npm:^1.0.2"
"@vitest/utils": "npm:4.1.9"
"@vitest/utils": "npm:4.1.10"
ast-v8-to-istanbul: "npm:^1.0.0"
istanbul-lib-coverage: "npm:^3.2.2"
istanbul-lib-report: "npm:^3.0.1"
@@ -6157,34 +6157,34 @@ __metadata:
std-env: "npm:^4.0.0-rc.1"
tinyrainbow: "npm:^3.1.0"
peerDependencies:
"@vitest/browser": 4.1.9
vitest: 4.1.9
"@vitest/browser": 4.1.10
vitest: 4.1.10
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10/1f236e17336973868aa6e7662b863b1c519d07840107daab3465429652741fc1f8a8988d1b7b28b8cfa883c7280f7479927a85e56c7874a0ddc5cc8ceda25cd6
checksum: 10/e593f5205a65d10f200e68a99e720d7a9f5e9be65d8160e4f0b6b5f1a7a87f0453c03e79fd1c022dbd7fb26a22657ca4d9410ae27b36da90d41d7ca04f681ab1
languageName: node
linkType: hard
"@vitest/expect@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/expect@npm:4.1.9"
"@vitest/expect@npm:4.1.10":
version: 4.1.10
resolution: "@vitest/expect@npm:4.1.10"
dependencies:
"@standard-schema/spec": "npm:^1.1.0"
"@types/chai": "npm:^5.2.2"
"@vitest/spy": "npm:4.1.9"
"@vitest/utils": "npm:4.1.9"
"@vitest/spy": "npm:4.1.10"
"@vitest/utils": "npm:4.1.10"
chai: "npm:^6.2.2"
tinyrainbow: "npm:^3.1.0"
checksum: 10/aba1a06cd28199f9c861d97797b014c0584fa6f6197e78345da0db5f74914d47f18958bb848658e889ca44452aa61e07ae851c16ea7b2175afd50d649dd4ed8c
checksum: 10/487fcad404a68968a54ae5fb9d099f12170cd793420a04b34a5606516317090c50a8303ab687c70166ee181864e3e138941d4a96d0405434dcd37696b3105350
languageName: node
linkType: hard
"@vitest/mocker@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/mocker@npm:4.1.9"
"@vitest/mocker@npm:4.1.10":
version: 4.1.10
resolution: "@vitest/mocker@npm:4.1.10"
dependencies:
"@vitest/spy": "npm:4.1.9"
"@vitest/spy": "npm:4.1.10"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.21"
peerDependencies:
@@ -6195,56 +6195,56 @@ __metadata:
optional: true
vite:
optional: true
checksum: 10/3e35ff3e2ecbdfbcae598e9c5c83978dd5f0cf3b16df37cf947c80faabce797ab275ca2075c3bb8ca85f595f3070267f93cb6798bbe415f1af2698f51833974c
checksum: 10/ae9645d1bcdad3ab7de7182feb4f1c9148a5ff97cef19581eec9257112aace94889eee9a1ad12e40ce59453ac05f52453b5fdb49ff76a31af8ccdbaaa4471ef3
languageName: node
linkType: hard
"@vitest/pretty-format@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/pretty-format@npm:4.1.9"
"@vitest/pretty-format@npm:4.1.10":
version: 4.1.10
resolution: "@vitest/pretty-format@npm:4.1.10"
dependencies:
tinyrainbow: "npm:^3.1.0"
checksum: 10/52512b300c000594c54bebbbfe31fab39e416a35d3686e2c46bc8e48ef8476d32306605f7736139608c3962943e0d22790dc15a3e6b1ffa436143d31f743a7c8
checksum: 10/e4f6907143ab0e40dda29d70b17027586c92921d622091321f10512e660b3995dcee7aa56e17b750b72560f295e25f96035372348415f18ebfd39b66a55b4704
languageName: node
linkType: hard
"@vitest/runner@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/runner@npm:4.1.9"
"@vitest/runner@npm:4.1.10":
version: 4.1.10
resolution: "@vitest/runner@npm:4.1.10"
dependencies:
"@vitest/utils": "npm:4.1.9"
"@vitest/utils": "npm:4.1.10"
pathe: "npm:^2.0.3"
checksum: 10/52e4e16e627faa62676f17683e570f505d58d2ce0ef421a3ae60e70c0ec5606d4af090fa6c7d5717d6e949f4401d6357b1f69cf06e52a5455a0ad9c9040268c0
checksum: 10/2c962cb13af0880990036808a35679b7ac6657c8f542490234c2faa6ffd2ab080ac6bf21b487c64d84aa635cfb37b49eb679098c2003a100dfc6c4d5e87bf055
languageName: node
linkType: hard
"@vitest/snapshot@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/snapshot@npm:4.1.9"
"@vitest/snapshot@npm:4.1.10":
version: 4.1.10
resolution: "@vitest/snapshot@npm:4.1.10"
dependencies:
"@vitest/pretty-format": "npm:4.1.9"
"@vitest/utils": "npm:4.1.9"
"@vitest/pretty-format": "npm:4.1.10"
"@vitest/utils": "npm:4.1.10"
magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3"
checksum: 10/c83349b1ad08d48284c1d3393168a7b7faffd24ace1ef337751a568dad322d83b0f9bc29378a4a60379cf2a13a268092b1d802936d6adb1ca28859f02dad8b87
checksum: 10/7940d83ffd2fbebf9a04ea31e196b7e8bf981093ec739950959fe8dd29caa33c80823780fb4b1063d9459c44a0a8d8b2748c00dfb6941becd7404e6d687eea01
languageName: node
linkType: hard
"@vitest/spy@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/spy@npm:4.1.9"
checksum: 10/8b8e42cc8e4b20d29bd8b312f34b9dbf2e20d4b4cdc24e3bcf6fd4d3b1f49e8924636d2730cca3946fbb45de893dfb531c77b832eb853c2624fdc2b800444e75
"@vitest/spy@npm:4.1.10":
version: 4.1.10
resolution: "@vitest/spy@npm:4.1.10"
checksum: 10/7c1b79a95474338e0659f0f2e43be4df1ef7939ff5b37b044954e0287582947803bd417508f44a7f244809672309d9b3dd67660b704ec3fe7f323cc958ae47a3
languageName: node
linkType: hard
"@vitest/utils@npm:4.1.9":
version: 4.1.9
resolution: "@vitest/utils@npm:4.1.9"
"@vitest/utils@npm:4.1.10":
version: 4.1.10
resolution: "@vitest/utils@npm:4.1.10"
dependencies:
"@vitest/pretty-format": "npm:4.1.9"
"@vitest/pretty-format": "npm:4.1.10"
convert-source-map: "npm:^2.0.0"
tinyrainbow: "npm:^3.1.0"
checksum: 10/78f5969fc09b1a95fda9dadd37e84a3a6ead35f66af15ad3b792eef35f80407047803e7afd53df86a8d794f59bf25ffbdc4146099140a3d5f9b51ea061bf2308
checksum: 10/95484aad55c7b00bbcd4963e27cbb86fe207620a6093973d68da9d0a06bad37c388d84c9ab43d5f35d88e46c8f376a5592d9c54c025c958361160f4802bb25ee
languageName: node
linkType: hard
@@ -9732,7 +9732,7 @@ __metadata:
"@codemirror/lint": "npm:6.9.7"
"@codemirror/search": "npm:6.7.1"
"@codemirror/state": "npm:6.7.1"
"@codemirror/view": "npm:6.43.5"
"@codemirror/view": "npm:6.43.6"
"@date-fns/tz": "npm:1.5.0"
"@egjs/hammerjs": "npm:2.0.17"
"@eslint/js": "npm:10.0.1"
@@ -9793,7 +9793,7 @@ __metadata:
"@types/sortablejs": "npm:1.15.9"
"@types/tar": "npm:7.0.87"
"@vibrant/color": "npm:4.0.4"
"@vitest/coverage-v8": "npm:4.1.9"
"@vitest/coverage-v8": "npm:4.1.10"
"@vvo/tzdb": "npm:6.198.0"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0"
@@ -9879,7 +9879,7 @@ __metadata:
typescript: "npm:6.0.3"
typescript-eslint: "npm:8.62.1"
vite-tsconfig-paths: "npm:6.1.1"
vitest: "npm:4.1.9"
vitest: "npm:4.1.10"
webpack-stats-plugin: "npm:1.1.3"
webpackbar: "npm:7.0.0"
weekstart: "npm:2.0.0"
@@ -15367,17 +15367,17 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:4.1.9":
version: 4.1.9
resolution: "vitest@npm:4.1.9"
"vitest@npm:4.1.10":
version: 4.1.10
resolution: "vitest@npm:4.1.10"
dependencies:
"@vitest/expect": "npm:4.1.9"
"@vitest/mocker": "npm:4.1.9"
"@vitest/pretty-format": "npm:4.1.9"
"@vitest/runner": "npm:4.1.9"
"@vitest/snapshot": "npm:4.1.9"
"@vitest/spy": "npm:4.1.9"
"@vitest/utils": "npm:4.1.9"
"@vitest/expect": "npm:4.1.10"
"@vitest/mocker": "npm:4.1.10"
"@vitest/pretty-format": "npm:4.1.10"
"@vitest/runner": "npm:4.1.10"
"@vitest/snapshot": "npm:4.1.10"
"@vitest/spy": "npm:4.1.10"
"@vitest/utils": "npm:4.1.10"
es-module-lexer: "npm:^2.0.0"
expect-type: "npm:^1.3.0"
magic-string: "npm:^0.30.21"
@@ -15395,12 +15395,12 @@ __metadata:
"@edge-runtime/vm": "*"
"@opentelemetry/api": ^1.9.0
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 4.1.9
"@vitest/browser-preview": 4.1.9
"@vitest/browser-webdriverio": 4.1.9
"@vitest/coverage-istanbul": 4.1.9
"@vitest/coverage-v8": 4.1.9
"@vitest/ui": 4.1.9
"@vitest/browser-playwright": 4.1.10
"@vitest/browser-preview": 4.1.10
"@vitest/browser-webdriverio": 4.1.10
"@vitest/coverage-istanbul": 4.1.10
"@vitest/coverage-v8": 4.1.10
"@vitest/ui": 4.1.10
happy-dom: "*"
jsdom: "*"
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -15431,7 +15431,7 @@ __metadata:
optional: false
bin:
vitest: ./vitest.mjs
checksum: 10/64f9d1a0aae92c493c39822ecae8ec5b5a336fc27166f776d08c01ae79ef1ec5485a1826ef1451bb05df2edaae109894125c2ecceadaa56c17be2690f66f9758
checksum: 10/020843460fe696c23be2a363634dde4daf54625f1c443c24066ba3f87c478b0ccfdd5124343ba30eb092f54902ffea09f4bed0af4a16a9ee805e494ee2dce34e
languageName: node
linkType: hard