|
@@ -17,6 +17,7 @@ SERVICE_SET_CODE = "set_code"
|
|
|
SERVICE_CLEAR_CODE = "clear_code"
|
|
SERVICE_CLEAR_CODE = "clear_code"
|
|
|
SERVICE_SET_NAME = "set_name"
|
|
SERVICE_SET_NAME = "set_name"
|
|
|
SERVICE_SET_SLOT_ENABLED = "set_slot_enabled"
|
|
SERVICE_SET_SLOT_ENABLED = "set_slot_enabled"
|
|
|
|
|
+SERVICE_COPY_SLOTS = "copy_slots"
|
|
|
|
|
|
|
|
_TARGET_FIELDS = {
|
|
_TARGET_FIELDS = {
|
|
|
vol.Exclusive("lock_entity_id", "target"): cv.entity_id,
|
|
vol.Exclusive("lock_entity_id", "target"): cv.entity_id,
|
|
@@ -59,6 +60,11 @@ SET_SLOT_ENABLED_SCHEMA = vol.All(
|
|
|
}),
|
|
}),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+COPY_SLOTS_SCHEMA = vol.Schema({
|
|
|
|
|
+ vol.Required("source_lock_entity_id"): cv.entity_id,
|
|
|
|
|
+ vol.Required("target_lock_entity_id"): cv.entity_id,
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
|
|
|
def _get_coordinator(hass: HomeAssistant, lock_entity_id: str) -> ZLockCoordinator:
|
|
def _get_coordinator(hass: HomeAssistant, lock_entity_id: str) -> ZLockCoordinator:
|
|
|
for entry_data in hass.data.get(DOMAIN, {}).values():
|
|
for entry_data in hass.data.get(DOMAIN, {}).values():
|
|
@@ -161,13 +167,67 @@ def register_services(hass: HomeAssistant) -> None:
|
|
|
|
|
|
|
|
await _fanout(lock_ids, _set_enabled)
|
|
await _fanout(lock_ids, _set_enabled)
|
|
|
|
|
|
|
|
|
|
+ async def handle_copy_slots(call: ServiceCall) -> None:
|
|
|
|
|
+ """Mirror one lock's entire Z-Wave user-slot profile onto another.
|
|
|
|
|
+
|
|
|
|
|
+ A hardware-migration helper (e.g. replacing lock A with lock B), not a
|
|
|
|
|
+ routine card operation: it talks to node.access_control directly rather
|
|
|
|
|
+ than the zwave_js.set_lock_usercode/clear_lock_usercode HA services used
|
|
|
|
|
+ by set_code/clear_code, and does a full mirror — slots on the target with
|
|
|
|
|
+ no counterpart on the source are deleted, not just left alone.
|
|
|
|
|
+ """
|
|
|
|
|
+ from zwave_js_server.model.access_control import SetUserOptions, UserCredentialType
|
|
|
|
|
+ from homeassistant.components.zwave_js.helpers import async_get_node_from_entity_id
|
|
|
|
|
+
|
|
|
|
|
+ source_lock_entity_id = call.data["source_lock_entity_id"]
|
|
|
|
|
+ target_lock_entity_id = call.data["target_lock_entity_id"]
|
|
|
|
|
+ if source_lock_entity_id == target_lock_entity_id:
|
|
|
|
|
+ raise ServiceValidationError("source_lock_entity_id and target_lock_entity_id must differ")
|
|
|
|
|
+
|
|
|
|
|
+ coordinator = _get_coordinator(hass, source_lock_entity_id)
|
|
|
|
|
+ _get_coordinator(hass, target_lock_entity_id)
|
|
|
|
|
+
|
|
|
|
|
+ source_node = async_get_node_from_entity_id(hass, source_lock_entity_id)
|
|
|
|
|
+ target_node = async_get_node_from_entity_id(hass, target_lock_entity_id)
|
|
|
|
|
+
|
|
|
|
|
+ source_users = [u for u in await source_node.access_control.get_users() if u.user_id != 0]
|
|
|
|
|
+ target_users = [u for u in await target_node.access_control.get_users() if u.user_id != 0]
|
|
|
|
|
+
|
|
|
|
|
+ source_slots = {u.user_id for u in source_users}
|
|
|
|
|
+ for user in target_users:
|
|
|
|
|
+ if user.user_id not in source_slots:
|
|
|
|
|
+ await target_node.access_control.delete_user(user.user_id)
|
|
|
|
|
+ await coordinator.async_set_name(target_lock_entity_id, user.user_id, "")
|
|
|
|
|
+
|
|
|
|
|
+ for user in source_users:
|
|
|
|
|
+ slot = user.user_id
|
|
|
|
|
+ await target_node.access_control.set_user(slot, SetUserOptions(
|
|
|
|
|
+ active=user.active,
|
|
|
|
|
+ user_type=user.user_type,
|
|
|
|
|
+ credential_rule=user.credential_rule,
|
|
|
|
|
+ expiring_timeout_minutes=user.expiring_timeout_minutes,
|
|
|
|
|
+ ))
|
|
|
|
|
+
|
|
|
|
|
+ credentials = await source_node.access_control.get_credentials(slot)
|
|
|
|
|
+ pin = next((c.data for c in credentials if c.type == UserCredentialType.PIN_CODE), None)
|
|
|
|
|
+ if pin is not None:
|
|
|
|
|
+ await target_node.access_control.set_credential(slot, UserCredentialType.PIN_CODE, slot, pin)
|
|
|
|
|
+
|
|
|
|
|
+ name = coordinator.get_name(source_lock_entity_id, slot)
|
|
|
|
|
+ await coordinator.async_set_name(target_lock_entity_id, slot, name or "")
|
|
|
|
|
+
|
|
|
|
|
+ await coordinator.async_refresh_lock(target_lock_entity_id)
|
|
|
|
|
+
|
|
|
hass.services.async_register(DOMAIN, SERVICE_SET_CODE, handle_set_code, SET_CODE_SCHEMA)
|
|
hass.services.async_register(DOMAIN, SERVICE_SET_CODE, handle_set_code, SET_CODE_SCHEMA)
|
|
|
hass.services.async_register(DOMAIN, SERVICE_CLEAR_CODE, handle_clear_code, CLEAR_CODE_SCHEMA)
|
|
hass.services.async_register(DOMAIN, SERVICE_CLEAR_CODE, handle_clear_code, CLEAR_CODE_SCHEMA)
|
|
|
hass.services.async_register(DOMAIN, SERVICE_SET_NAME, handle_set_name, SET_NAME_SCHEMA)
|
|
hass.services.async_register(DOMAIN, SERVICE_SET_NAME, handle_set_name, SET_NAME_SCHEMA)
|
|
|
hass.services.async_register(DOMAIN, SERVICE_SET_SLOT_ENABLED, handle_set_slot_enabled, SET_SLOT_ENABLED_SCHEMA)
|
|
hass.services.async_register(DOMAIN, SERVICE_SET_SLOT_ENABLED, handle_set_slot_enabled, SET_SLOT_ENABLED_SCHEMA)
|
|
|
|
|
+ hass.services.async_register(DOMAIN, SERVICE_COPY_SLOTS, handle_copy_slots, COPY_SLOTS_SCHEMA)
|
|
|
|
|
|
|
|
|
|
|
|
|
def unregister_services(hass: HomeAssistant) -> None:
|
|
def unregister_services(hass: HomeAssistant) -> None:
|
|
|
- for service in (SERVICE_SET_CODE, SERVICE_CLEAR_CODE, SERVICE_SET_NAME, SERVICE_SET_SLOT_ENABLED):
|
|
|
|
|
|
|
+ for service in (
|
|
|
|
|
+ SERVICE_SET_CODE, SERVICE_CLEAR_CODE, SERVICE_SET_NAME, SERVICE_SET_SLOT_ENABLED, SERVICE_COPY_SLOTS,
|
|
|
|
|
+ ):
|
|
|
if hass.services.has_service(DOMAIN, service):
|
|
if hass.services.has_service(DOMAIN, service):
|
|
|
hass.services.async_remove(DOMAIN, service)
|
|
hass.services.async_remove(DOMAIN, service)
|