diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index d607f87b76a..d041b713125 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -83,3 +83,10 @@ EVENT_TYPE_FINGERPRINT_IDENTIFIED: Final = "identified" EVENT_TYPE_FINGERPRINT_NOT_IDENTIFIED: Final = "not_identified" EVENT_TYPE_NFC_SCANNED: Final = "scanned" EVENT_TYPE_DOORBELL_RING: Final = "ring" + +KEYRINGS_ULP_ID: Final = "ulp_id" +KEYRINGS_USER_STATUS: Final = "user_status" +KEYRINGS_USER_FULL_NAME: Final = "full_name" +KEYRINGS_KEY_TYPE: Final = "key_type" +KEYRINGS_KEY_TYPE_ID_FINGERPRINT: Final = "fingerprint_id" +KEYRINGS_KEY_TYPE_ID_NFC: Final = "nfc_id" diff --git a/homeassistant/components/unifiprotect/icons.json b/homeassistant/components/unifiprotect/icons.json index 5e80e3095b3..b5e8277d82a 100644 --- a/homeassistant/components/unifiprotect/icons.json +++ b/homeassistant/components/unifiprotect/icons.json @@ -11,6 +11,9 @@ }, "remove_privacy_zone": { "service": "mdi:eye-minus" + }, + "get_user_keyring_info": { + "service": "mdi:key-chain" } } } diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 35713efdf3d..6a1daef178e 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -13,7 +13,13 @@ import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME, Platform -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import ( config_validation as cv, @@ -21,9 +27,19 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.service import async_extract_referenced_entity_ids +from homeassistant.util.json import JsonValueType from homeassistant.util.read_only_dict import ReadOnlyDict -from .const import ATTR_MESSAGE, DOMAIN +from .const import ( + ATTR_MESSAGE, + DOMAIN, + KEYRINGS_KEY_TYPE, + KEYRINGS_KEY_TYPE_ID_FINGERPRINT, + KEYRINGS_KEY_TYPE_ID_NFC, + KEYRINGS_ULP_ID, + KEYRINGS_USER_FULL_NAME, + KEYRINGS_USER_STATUS, +) from .data import async_ufp_instance_for_config_entry_ids SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" @@ -31,12 +47,14 @@ SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" SERVICE_SET_PRIVACY_ZONE = "set_privacy_zone" SERVICE_REMOVE_PRIVACY_ZONE = "remove_privacy_zone" SERVICE_SET_CHIME_PAIRED = "set_chime_paired_doorbells" +SERVICE_GET_USER_KEYRING_INFO = "get_user_keyring_info" ALL_GLOBAL_SERIVCES = [ SERVICE_ADD_DOORBELL_TEXT, SERVICE_REMOVE_DOORBELL_TEXT, SERVICE_SET_CHIME_PAIRED, SERVICE_REMOVE_PRIVACY_ZONE, + SERVICE_GET_USER_KEYRING_INFO, ] DOORBELL_TEXT_SCHEMA = vol.All( @@ -69,6 +87,15 @@ REMOVE_PRIVACY_ZONE_SCHEMA = vol.All( cv.has_at_least_one_key(ATTR_DEVICE_ID), ) +GET_USER_KEYRING_INFO_SCHEMA = vol.All( + vol.Schema( + { + **cv.ENTITY_SERVICE_FIELDS, + }, + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID), +) + @callback def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiClient: @@ -205,26 +232,70 @@ async def set_chime_paired_doorbells(call: ServiceCall) -> None: await chime.save_device(data_before_changed) +async def get_user_keyring_info(call: ServiceCall) -> ServiceResponse: + """Get the user keyring info.""" + camera = _async_get_ufp_camera(call) + ulp_users = camera.api.bootstrap.ulp_users.as_list() + user_keyrings: list[JsonValueType] = [ + { + KEYRINGS_USER_FULL_NAME: user.full_name, + KEYRINGS_USER_STATUS: user.status, + KEYRINGS_ULP_ID: user.ulp_id, + "keys": [ + { + KEYRINGS_KEY_TYPE: key.registry_type, + **( + {KEYRINGS_KEY_TYPE_ID_FINGERPRINT: key.registry_id} + if key.registry_type == "fingerprint" + else {} + ), + **( + {KEYRINGS_KEY_TYPE_ID_NFC: key.registry_id} + if key.registry_type == "nfc" + else {} + ), + } + for key in camera.api.bootstrap.keyrings.as_list() + if key.ulp_user == user.ulp_id + ], + } + for user in ulp_users + ] + + response: ServiceResponse = {"users": user_keyrings} + return response + + SERVICES = [ ( SERVICE_ADD_DOORBELL_TEXT, add_doorbell_text, DOORBELL_TEXT_SCHEMA, + SupportsResponse.NONE, ), ( SERVICE_REMOVE_DOORBELL_TEXT, remove_doorbell_text, DOORBELL_TEXT_SCHEMA, + SupportsResponse.NONE, ), ( SERVICE_SET_CHIME_PAIRED, set_chime_paired_doorbells, CHIME_PAIRED_SCHEMA, + SupportsResponse.NONE, ), ( SERVICE_REMOVE_PRIVACY_ZONE, remove_privacy_zone, REMOVE_PRIVACY_ZONE_SCHEMA, + SupportsResponse.NONE, + ), + ( + SERVICE_GET_USER_KEYRING_INFO, + get_user_keyring_info, + GET_USER_KEYRING_INFO_SCHEMA, + SupportsResponse.ONLY, ), ] @@ -232,5 +303,7 @@ SERVICES = [ def async_setup_services(hass: HomeAssistant) -> None: """Set up the global UniFi Protect services.""" - for name, method, schema in SERVICES: - hass.services.async_register(DOMAIN, name, method, schema=schema) + for name, method, schema, supports_response in SERVICES: + hass.services.async_register( + DOMAIN, name, method, schema=schema, supports_response=supports_response + ) diff --git a/homeassistant/components/unifiprotect/services.yaml b/homeassistant/components/unifiprotect/services.yaml index 192dfd0566f..b620c195fc2 100644 --- a/homeassistant/components/unifiprotect/services.yaml +++ b/homeassistant/components/unifiprotect/services.yaml @@ -53,3 +53,10 @@ remove_privacy_zone: required: true selector: text: +get_user_keyring_info: + fields: + device_id: + required: true + selector: + device: + integration: unifiprotect diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 8ecb4076409..cde8c88d169 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -225,6 +225,16 @@ "description": "The name of the zone to remove." } } + }, + "get_user_keyring_info": { + "name": "Retrieve Keyring Details for Users", + "description": "Fetch a detailed list of users with NFC and fingerprint associations for automations.", + "fields": { + "device_id": { + "name": "UniFi Protect NVR", + "description": "Any device from the UniFi Protect instance you want to retrieve keyring details. This is useful for systems with multiple Protect instances." + } + } } } } diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 84e0e74a492..efc9d1ace9e 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -9,9 +9,19 @@ from uiprotect.data import Camera, Chime, Color, Light, ModelType from uiprotect.data.devices import CameraZone from uiprotect.exceptions import BadRequest -from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN +from homeassistant.components.unifiprotect.const import ( + ATTR_MESSAGE, + DOMAIN, + KEYRINGS_KEY_TYPE, + KEYRINGS_KEY_TYPE_ID_FINGERPRINT, + KEYRINGS_KEY_TYPE_ID_NFC, + KEYRINGS_ULP_ID, + KEYRINGS_USER_FULL_NAME, + KEYRINGS_USER_STATUS, +) from homeassistant.components.unifiprotect.services import ( SERVICE_ADD_DOORBELL_TEXT, + SERVICE_GET_USER_KEYRING_INFO, SERVICE_REMOVE_DOORBELL_TEXT, SERVICE_REMOVE_PRIVACY_ZONE, SERVICE_SET_CHIME_PAIRED, @@ -249,3 +259,59 @@ async def test_remove_privacy_zone( ) ufp.api.update_device.assert_called() assert not doorbell.privacy_zones + + +@pytest.mark.asyncio +async def test_get_doorbell_user( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + ufp: MockUFPFixture, + doorbell: Camera, +) -> None: + """Test get_doorbell_user service.""" + + ulp_user = Mock(full_name="Test User", status="active", ulp_id="user_ulp_id") + keyring = Mock( + registry_type="nfc", + registry_id="123456", + ulp_user="user_ulp_id", + ) + keyring_2 = Mock( + registry_type="fingerprint", + registry_id="2", + ulp_user="user_ulp_id", + ) + ufp.api.bootstrap.ulp_users.as_list = Mock(return_value=[ulp_user]) + ufp.api.bootstrap.keyrings.as_list = Mock(return_value=[keyring, keyring_2]) + + await init_entry(hass, ufp, [doorbell]) + + camera_entry = entity_registry.async_get("binary_sensor.test_camera_doorbell") + + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_USER_KEYRING_INFO, + {ATTR_DEVICE_ID: camera_entry.device_id}, + blocking=True, + return_response=True, + ) + + assert response == { + "users": [ + { + KEYRINGS_USER_FULL_NAME: "Test User", + "keys": [ + { + KEYRINGS_KEY_TYPE: "nfc", + KEYRINGS_KEY_TYPE_ID_NFC: "123456", + }, + { + KEYRINGS_KEY_TYPE_ID_FINGERPRINT: "2", + KEYRINGS_KEY_TYPE: "fingerprint", + }, + ], + KEYRINGS_USER_STATUS: "active", + KEYRINGS_ULP_ID: "user_ulp_id", + }, + ], + }