Skip to content

Commit b7fdd63

Browse files
committed
Z-Wave: use add_user service to add users with credentials
1 parent 70ffef8 commit b7fdd63

3 files changed

Lines changed: 91 additions & 17 deletions

File tree

src/data/zwave_js-credentials.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ export interface SetZwaveUserResult {
126126
user_id: number;
127127
}
128128

129+
export interface AddZwaveUserParams {
130+
user_name?: string | null;
131+
user_type?: string;
132+
credential_rule?: string;
133+
active?: boolean;
134+
credential_type: ZwaveCredentialType;
135+
credential_data: string;
136+
}
137+
138+
export interface AddZwaveUserResult {
139+
user_id: number;
140+
credential_slot: number;
141+
}
142+
129143
export interface SetZwaveCredentialParams {
130144
user_id: number;
131145
credential_type: ZwaveCredentialType;
@@ -216,6 +230,23 @@ export const setZwaveUser = async (
216230
return unwrapEntityResponse(hass, result.response, entity_id);
217231
};
218232

233+
export const addZwaveUser = async (
234+
hass: HomeAssistant,
235+
entity_id: string,
236+
params: AddZwaveUserParams
237+
): Promise<AddZwaveUserResult> => {
238+
// notifyOnError=false — caller surfaces errors in-dialog instead.
239+
const result = await hass.callService<Record<string, AddZwaveUserResult>>(
240+
"zwave_js",
241+
"add_user",
242+
params,
243+
{ entity_id },
244+
false,
245+
true
246+
);
247+
return unwrapEntityResponse(hass, result.response, entity_id);
248+
};
249+
219250
export const deleteZwaveUser = (
220251
hass: HomeAssistant,
221252
entity_id: string,

src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-credential-user-edit.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import {
1818
DEFAULT_CREDENTIAL_MAX_LENGTH,
1919
DEFAULT_CREDENTIAL_MIN_LENGTH,
2020
ENTERABLE_ZWAVE_CREDENTIAL_TYPES,
21+
addZwaveUser,
2122
deleteZwaveCredential,
22-
deleteZwaveUser,
2323
enterableCredentialTypes,
2424
getCredentialError,
2525
compatibleUserTypes,
@@ -508,27 +508,19 @@ class DialogZwaveCredentialUserEdit extends LitElement {
508508
credentialType: ZwaveCredentialType
509509
): Promise<void> {
510510
const params = this._params!;
511-
const { user_id } = await setZwaveUser(this.hass, params.entity_id, {
512-
user_name: this._supportsUserNames ? this._userName.trim() : undefined,
513-
user_type: this._userType,
514-
active: true,
515-
});
516-
511+
// Create the user and write the credential in a single call. This is the
512+
// only way to add a user on User Code CC locks, where the user and code
513+
// share a slot. If the credential write fails, the service rolls the user
514+
// back so the lock is never left with a credential-less user.
517515
try {
518-
await setZwaveCredential(this.hass, params.entity_id, {
519-
user_id,
516+
await addZwaveUser(this.hass, params.entity_id, {
517+
user_name: this._supportsUserNames ? this._userName.trim() : undefined,
518+
user_type: this._userType,
519+
active: true,
520520
credential_type: credentialType,
521521
credential_data: this._credentialData,
522522
});
523523
} catch (err: unknown) {
524-
// Roll back the user so the lock returns to its prior state. We
525-
// ignore rollback errors — the credential error is the actionable
526-
// one to surface; a stranded user will reappear on next refresh.
527-
try {
528-
await deleteZwaveUser(this.hass, params.entity_id, user_id);
529-
} catch {
530-
// Ignore.
531-
}
532524
this._error = this.hass.localize(
533525
"ui.panel.config.zwave_js.credentials.errors.add_user_failed",
534526
{

test/data/zwave_js-credentials.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
getZwaveCredentialCapabilities,
44
getZwaveUsers,
55
setZwaveUser,
6+
addZwaveUser,
67
deleteZwaveUser,
78
deleteZwaveAllUsers,
89
setZwaveCredential,
@@ -171,6 +172,56 @@ describe("zwave_js-credentials", () => {
171172
});
172173
});
173174

175+
describe("addZwaveUser", () => {
176+
const addUserResponse = (inner: unknown) =>
177+
({
178+
callService: vi
179+
.fn()
180+
.mockResolvedValue({ response: { [ENTITY_ID]: inner } }),
181+
}) as unknown as HomeAssistant;
182+
183+
it("calls add_user with user and credential, returning the slot", async () => {
184+
const hass = addUserResponse({ user_id: 1, credential_slot: 1 });
185+
186+
const result = await addZwaveUser(hass, ENTITY_ID, {
187+
user_name: "Alice",
188+
user_type: "general",
189+
active: true,
190+
credential_type: "pin_code",
191+
credential_data: "1234",
192+
});
193+
194+
expect(hass.callService).toHaveBeenCalledWith(
195+
"zwave_js",
196+
"add_user",
197+
{
198+
user_name: "Alice",
199+
user_type: "general",
200+
active: true,
201+
credential_type: "pin_code",
202+
credential_data: "1234",
203+
},
204+
{ entity_id: ENTITY_ID },
205+
false,
206+
true
207+
);
208+
expect(result).toEqual({ user_id: 1, credential_slot: 1 });
209+
});
210+
211+
it("propagates errors from callService", async () => {
212+
const hass = {
213+
callService: vi.fn().mockRejectedValue(new Error("No slots")),
214+
} as unknown as HomeAssistant;
215+
216+
await expect(
217+
addZwaveUser(hass, ENTITY_ID, {
218+
credential_type: "pin_code",
219+
credential_data: "1234",
220+
})
221+
).rejects.toThrow("No slots");
222+
});
223+
});
224+
174225
describe("deleteZwaveUser", () => {
175226
it("calls the correct service with user_id and entity_id target", async () => {
176227
const hass = mockHass();

0 commit comments

Comments
 (0)