diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 599753cc1cc..19daebf9ce1 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -31,7 +31,7 @@ BINARY_SENSOR_TYPES: tuple[RingBinarySensorEntityDescription, ...] = ( RingBinarySensorEntityDescription( key="ding", translation_key="ding", - category=["doorbots", "authorized_doorbots"], + category=["doorbots", "authorized_doorbots", "other"], device_class=BinarySensorDeviceClass.OCCUPANCY, ), RingBinarySensorEntityDescription( @@ -56,7 +56,7 @@ async def async_setup_entry( entities = [ RingBinarySensor(ring, device, notifications_coordinator, description) - for device_type in ("doorbots", "authorized_doorbots", "stickup_cams") + for device_type in ("doorbots", "authorized_doorbots", "stickup_cams", "other") for description in BINARY_SENSOR_TYPES if device_type in description.category for device in devices[device_type] diff --git a/homeassistant/components/ring/icons.json b/homeassistant/components/ring/icons.json index 07b42db1516..9ce0de6bebc 100644 --- a/homeassistant/components/ring/icons.json +++ b/homeassistant/components/ring/icons.json @@ -13,6 +13,15 @@ "volume": { "default": "mdi:bell-ring" }, + "doorbell_volume": { + "default": "mdi:bell-ring" + }, + "mic_volume": { + "default": "mdi:microphone" + }, + "voice_volume": { + "default": "mdi:account-voice" + }, "wifi_signal_category": { "default": "mdi:wifi" }, diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 874d3664ab7..9ba677e7e5b 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -40,7 +40,13 @@ async def async_setup_entry( entities = [ description.cls(device, devices_coordinator, description) - for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams") + for device_type in ( + "chimes", + "doorbots", + "authorized_doorbots", + "stickup_cams", + "other", + ) for description in SENSOR_TYPES if device_type in description.category for device in devices[device_type] @@ -72,6 +78,12 @@ class RingSensor(RingEntity, SensorEntity): sensor_type = self.entity_description.key if sensor_type == "volume": return self._device.volume + if sensor_type == "doorbell_volume": + return self._device.doorbell_volume + if sensor_type == "mic_volume": + return self._device.mic_volume + if sensor_type == "voice_volume": + return self._device.voice_volume if sensor_type == "battery": return self._device.battery_life @@ -156,7 +168,7 @@ class RingSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( RingSensorEntityDescription( key="battery", - category=["doorbots", "authorized_doorbots", "stickup_cams"], + category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -166,14 +178,14 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( RingSensorEntityDescription( key="last_activity", translation_key="last_activity", - category=["doorbots", "authorized_doorbots", "stickup_cams"], + category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], device_class=SensorDeviceClass.TIMESTAMP, cls=HistoryRingSensor, ), RingSensorEntityDescription( key="last_ding", translation_key="last_ding", - category=["doorbots", "authorized_doorbots"], + category=["doorbots", "authorized_doorbots", "other"], kind="ding", device_class=SensorDeviceClass.TIMESTAMP, cls=HistoryRingSensor, @@ -192,17 +204,35 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], cls=RingSensor, ), + RingSensorEntityDescription( + key="doorbell_volume", + translation_key="doorbell_volume", + category=["other"], + cls=RingSensor, + ), + RingSensorEntityDescription( + key="mic_volume", + translation_key="mic_volume", + category=["other"], + cls=RingSensor, + ), + RingSensorEntityDescription( + key="voice_volume", + translation_key="voice_volume", + category=["other"], + cls=RingSensor, + ), RingSensorEntityDescription( key="wifi_signal_category", translation_key="wifi_signal_category", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], entity_category=EntityCategory.DIAGNOSTIC, cls=HealthDataRingSensor, ), RingSensorEntityDescription( key="wifi_signal_strength", translation_key="wifi_signal_strength", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json index ee7dbc000f5..de8b5112ec9 100644 --- a/homeassistant/components/ring/strings.json +++ b/homeassistant/components/ring/strings.json @@ -60,6 +60,15 @@ "volume": { "name": "Volume" }, + "doorbell_volume": { + "name": "Doorbell volume" + }, + "mic_volume": { + "name": "Mic volume" + }, + "voice_volume": { + "name": "Voice volume" + }, "wifi_signal_category": { "name": "Wi-Fi signal category" }, diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index 833d84265a6..106a824f1d5 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -96,7 +96,7 @@ def requests_mock_fixture(): re.compile( r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/history" ), - text=load_fixture("doorbots.json", "ring"), + text=load_fixture("doorbot_history.json", "ring"), ) # Mocks the response for getting the health of a device mock.get( @@ -115,6 +115,15 @@ def requests_mock_fixture(): status_code=200, json={"url": "http://127.0.0.1/foo"}, ) + mock.get( + "https://api.ring.com/groups/v1/locations/mock-location-id/groups", + text=load_fixture("groups.json", "ring"), + ) + # Mocks the response for getting the history of the intercom + mock.get( + "https://api.ring.com/clients_api/doorbots/185036587/history", + text=load_fixture("intercom_history.json", "ring"), + ) # Mocks the response for setting properties in settings (i.e. motion_detection) mock.patch( re.compile( diff --git a/tests/components/ring/fixtures/devices.json b/tests/components/ring/fixtures/devices.json index ae7a62e1bae..8deee7ec413 100644 --- a/tests/components/ring/fixtures/devices.json +++ b/tests/components/ring/fixtures/devices.json @@ -378,5 +378,92 @@ "subscribed_motions": true, "time_zone": "America/New_York" } + ], + "other": [ + { + "id": 185036587, + "kind": "intercom_handset_audio", + "description": "Ingress", + "location_id": "mock-location-id", + "schema_id": null, + "is_sidewalk_gateway": false, + "created_at": "2023-12-01T18:05:25Z", + "deactivated_at": null, + "owner": { + "id": 762490876, + "first_name": "", + "last_name": "", + "email": "" + }, + "device_id": "124ba1b3fe1a", + "time_zone": "Europe/Rome", + "firmware_version": "Up to Date", + "owned": true, + "ring_net_id": null, + "settings": { + "features_confirmed": 5, + "show_recordings": true, + "recording_ttl": 180, + "recording_enabled": false, + "keep_alive": null, + "keep_alive_auto": 45.0, + "doorbell_volume": 8, + "enable_chime": 1, + "theft_alarm_enable": 0, + "use_cached_domain": 1, + "use_server_ip": 0, + "server_domain": "fw.ring.com", + "server_ip": null, + "enable_log": 1, + "forced_keep_alive": null, + "mic_volume": 11, + "chime_settings": { + "enable": true, + "type": 2, + "duration": 10 + }, + "intercom_settings": { + "ring_to_open": false, + "predecessor": "{\"make\":\"Comelit\",\"model\":\"2738W\",\"wires\":2}", + "config": "{\"intercom_type\": 2, \"number_of_wires\": 2, \"autounlock_enabled\": false, \"speaker_gain\": [-49, -35, -25, -21, -16, -9, -6, -3, 0, 3, 6, 9], \"digital\": {\"audio_amp\": 0, \"chg_en\": false, \"fast_chg\": false, \"bypass\": false, \"idle_lvl\": 32, \"ext_audio\": false, \"ext_audio_term\": 0, \"off_hk_tm\": 0, \"unlk_ka\": false, \"unlock\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"ring\": {\"cap_tm\": 40, \"rpl_tm\": 200, \"gain\": 2000, \"cmp_thr\": 4500, \"lvl\": 28000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"m\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_off\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_on\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}}}", + "intercom_type": "DF", + "replication": 1, + "unlock_mode": 0 + }, + "voice_volume": 11 + }, + "alerts": { + "connection": "online", + "ota_status": "timeout" + }, + "function": { + "name": null + }, + "subscribed": false, + "battery_life": "52", + "features": { + "cfes_eligible": false, + "motion_zone_recommendation": false, + "motions_enabled": true, + "show_recordings": true, + "show_vod_settings": true, + "rich_notifications_eligible": false, + "show_offline_motion_events": false, + "sheila_camera_eligible": null, + "sheila_camera_processing_eligible": null, + "dynamic_network_switching_eligible": false, + "chime_auto_detect_capable": false, + "missing_key_delivery_address": false, + "show_24x7_lite": false, + "recording_24x7_eligible": null + }, + "metadata": { + "ethernet": false, + "legacy_fw_migrated": true, + "imported_from_amazon": false, + "is_sidewalk_gateway": false, + "key_access_point_associated": true + } + } ] } diff --git a/tests/components/ring/fixtures/doorbots.json b/tests/components/ring/fixtures/doorbot_history.json similarity index 100% rename from tests/components/ring/fixtures/doorbots.json rename to tests/components/ring/fixtures/doorbot_history.json diff --git a/tests/components/ring/fixtures/groups.json b/tests/components/ring/fixtures/groups.json new file mode 100644 index 00000000000..399aaac1641 --- /dev/null +++ b/tests/components/ring/fixtures/groups.json @@ -0,0 +1,24 @@ +{ + "device_groups": [ + { + "device_group_id": "mock-group-id", + "location_id": "mock-location-id", + "name": "Landscape", + "devices": [ + { + "doorbot_id": 12345678, + "location_id": "mock-location-id", + "type": "beams_ct200_transformer", + "mac_address": null, + "hardware_id": "1234567890", + "name": "Mock Transformer", + "deleted_at": null + } + ], + "created_at": "2020-11-03T22:07:05Z", + "updated_at": "2020-11-19T03:52:59Z", + "deleted_at": null, + "external_id": "12345678-1234-5678-90ab-1234567890ab" + } + ] +} diff --git a/tests/components/ring/fixtures/intercom_history.json b/tests/components/ring/fixtures/intercom_history.json new file mode 100644 index 00000000000..fccd87b9227 --- /dev/null +++ b/tests/components/ring/fixtures/intercom_history.json @@ -0,0 +1,116 @@ +[ + { + "id": 7330963245622279024, + "created_at": "2024-02-02T11:21:24.000Z", + "answered": false, + "events": [], + "kind": "ding", + "favorite": false, + "snapshot_url": "", + "recording": { + "status": "ready" + }, + "duration": 40.0, + "cv_properties": { + "person_detected": null, + "stream_broken": false, + "detection_type": null, + "cv_triggers": null, + "detection_types": null, + "security_alerts": null + }, + "properties": { + "is_alexa": false, + "is_sidewalk": false, + "is_autoreply": false + }, + "doorbot": { + "id": 185036587, + "description": "Ingresso", + "type": "intercom_handset_audio" + }, + "device_placement": null, + "geolocation": null, + "last_location": null, + "siren": null, + "is_e2ee": false, + "had_subscription": false, + "owner_id": "762490876" + }, + { + "id": 7323267080901445808, + "created_at": "2024-01-12T17:36:28.000Z", + "answered": true, + "events": [], + "kind": "on_demand", + "favorite": false, + "snapshot_url": "", + "recording": { + "status": "ready" + }, + "duration": 13.0, + "cv_properties": { + "person_detected": null, + "stream_broken": false, + "detection_type": null, + "cv_triggers": null, + "detection_types": null, + "security_alerts": null + }, + "properties": { + "is_alexa": false, + "is_sidewalk": false, + "is_autoreply": false + }, + "doorbot": { + "id": 185036587, + "description": "Ingresso", + "type": "intercom_handset_audio" + }, + "device_placement": null, + "geolocation": null, + "last_location": null, + "siren": null, + "is_e2ee": false, + "had_subscription": false, + "owner_id": "762490876" + }, + { + "id": 7307399027047288688, + "created_at": "2023-12-01T18:44:28.000Z", + "answered": true, + "events": [], + "kind": "on_demand", + "favorite": false, + "snapshot_url": "", + "recording": { + "status": "ready" + }, + "duration": 43.0, + "cv_properties": { + "person_detected": null, + "stream_broken": false, + "detection_type": null, + "cv_triggers": null, + "detection_types": null, + "security_alerts": null + }, + "properties": { + "is_alexa": false, + "is_sidewalk": false, + "is_autoreply": false + }, + "doorbot": { + "id": 185036587, + "description": "Ingresso", + "type": "intercom_handset_audio" + }, + "device_placement": null, + "geolocation": null, + "last_location": null, + "siren": null, + "is_e2ee": false, + "had_subscription": false, + "owner_id": "762490876" + } +] diff --git a/tests/components/ring/snapshots/test_diagnostics.ambr b/tests/components/ring/snapshots/test_diagnostics.ambr index 2b8f2bac389..1a9d8898af6 100644 --- a/tests/components/ring/snapshots/test_diagnostics.ambr +++ b/tests/components/ring/snapshots/test_diagnostics.ambr @@ -577,6 +577,91 @@ 'subscribed_motions': True, 'time_zone': 'America/New_York', }), + dict({ + 'alerts': dict({ + 'connection': 'online', + 'ota_status': 'timeout', + }), + 'battery_life': '52', + 'created_at': '2023-12-01T18:05:25Z', + 'deactivated_at': None, + 'description': '**REDACTED**', + 'device_id': '**REDACTED**', + 'features': dict({ + 'cfes_eligible': False, + 'chime_auto_detect_capable': False, + 'dynamic_network_switching_eligible': False, + 'missing_key_delivery_address': False, + 'motion_zone_recommendation': False, + 'motions_enabled': True, + 'recording_24x7_eligible': None, + 'rich_notifications_eligible': False, + 'sheila_camera_eligible': None, + 'sheila_camera_processing_eligible': None, + 'show_24x7_lite': False, + 'show_offline_motion_events': False, + 'show_recordings': True, + 'show_vod_settings': True, + }), + 'firmware_version': 'Up to Date', + 'function': dict({ + 'name': None, + }), + 'id': '**REDACTED**', + 'is_sidewalk_gateway': False, + 'kind': 'intercom_handset_audio', + 'location_id': '**REDACTED**', + 'metadata': dict({ + 'ethernet': False, + 'imported_from_amazon': False, + 'is_sidewalk_gateway': False, + 'key_access_point_associated': True, + 'legacy_fw_migrated': True, + }), + 'owned': True, + 'owner': dict({ + 'email': '', + 'first_name': '', + 'id': '**REDACTED**', + 'last_name': '', + }), + 'ring_net_id': None, + 'schema_id': None, + 'settings': dict({ + 'chime_settings': dict({ + 'duration': 10, + 'enable': True, + 'type': 2, + }), + 'doorbell_volume': 8, + 'enable_chime': 1, + 'enable_log': 1, + 'features_confirmed': 5, + 'forced_keep_alive': None, + 'intercom_settings': dict({ + 'config': '{"intercom_type": 2, "number_of_wires": 2, "autounlock_enabled": false, "speaker_gain": [-49, -35, -25, -21, -16, -9, -6, -3, 0, 3, 6, 9], "digital": {"audio_amp": 0, "chg_en": false, "fast_chg": false, "bypass": false, "idle_lvl": 32, "ext_audio": false, "ext_audio_term": 0, "off_hk_tm": 0, "unlk_ka": false, "unlock": {"cap_tm": 1000, "rpl_tm": 1500, "gain": 1000, "cmp_thr": 4000, "lvl": 25000, "thr": 30, "thr2": 0, "offln": false, "ac": true, "bias": "h", "tx_pin": "TXD2", "ack": 0, "prot": "Comelit_SB2", "ask": {"f": 25000, "b": 333}}, "ring": {"cap_tm": 40, "rpl_tm": 200, "gain": 2000, "cmp_thr": 4500, "lvl": 28000, "thr": 30, "thr2": 0, "offln": false, "ac": true, "bias": "m", "tx_pin": "TXD2", "ack": 0, "prot": "Comelit_SB2", "ask": {"f": 25000, "b": 333}}, "hook_off": {"cap_tm": 1000, "rpl_tm": 1500, "gain": 1000, "cmp_thr": 4000, "lvl": 25000, "thr": 30, "thr2": 0, "offln": false, "ac": true, "bias": "h", "tx_pin": "TXD2", "ack": 0, "prot": "Comelit_SB2", "ask": {"f": 25000, "b": 333}}, "hook_on": {"cap_tm": 1000, "rpl_tm": 1500, "gain": 1000, "cmp_thr": 4000, "lvl": 25000, "thr": 30, "thr2": 0, "offln": false, "ac": true, "bias": "h", "tx_pin": "TXD2", "ack": 0, "prot": "Comelit_SB2", "ask": {"f": 25000, "b": 333}}}}', + 'intercom_type': 'DF', + 'predecessor': '{"make":"Comelit","model":"2738W","wires":2}', + 'replication': 1, + 'ring_to_open': False, + 'unlock_mode': 0, + }), + 'keep_alive': None, + 'keep_alive_auto': 45.0, + 'mic_volume': 11, + 'recording_enabled': False, + 'recording_ttl': 180, + 'server_domain': 'fw.ring.com', + 'server_ip': None, + 'show_recordings': True, + 'theft_alarm_enable': 0, + 'use_cached_domain': 1, + 'use_server_ip': 0, + 'voice_volume': 11, + }), + 'subscribed': False, + 'time_zone': 'Europe/Rome', + }), ]), }) # --- diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 8738594fa05..ba73de05c9b 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -33,6 +33,10 @@ async def test_binary_sensor( assert motion_state.state == "on" assert motion_state.attributes["device_class"] == "motion" - ding_state = hass.states.get("binary_sensor.front_door_ding") - assert ding_state is not None - assert ding_state.state == "off" + front_ding_state = hass.states.get("binary_sensor.front_door_ding") + assert front_ding_state is not None + assert front_ding_state.state == "off" + + ingress_ding_state = hass.states.get("binary_sensor.ingress_ding") + assert ingress_ding_state is not None + assert ingress_ding_state.state == "off" diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 0299e626670..aadea6f0ba1 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -40,13 +40,19 @@ async def test_sensor(hass: HomeAssistant, requests_mock: requests_mock.Mocker) assert downstairs_volume_state is not None assert downstairs_volume_state.state == "2" - front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity") - assert front_door_last_activity_state is not None - downstairs_wifi_signal_strength_state = hass.states.get( "sensor.downstairs_wifi_signal_strength" ) + ingress_mic_volume_state = hass.states.get("sensor.ingress_mic_volume") + assert ingress_mic_volume_state.state == "11" + + ingress_doorbell_volume_state = hass.states.get("sensor.ingress_doorbell_volume") + assert ingress_doorbell_volume_state.state == "8" + + ingress_voice_volume_state = hass.states.get("sensor.ingress_voice_volume") + assert ingress_voice_volume_state.state == "11" + if not WIFI_ENABLED: return @@ -66,6 +72,24 @@ async def test_sensor(hass: HomeAssistant, requests_mock: requests_mock.Mocker) assert front_door_wifi_signal_strength_state.state == "-58" +async def test_history( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + requests_mock: requests_mock.Mocker, +) -> None: + """Test history derived sensors.""" + await setup_platform(hass, Platform.SENSOR) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(True) + + front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity") + assert front_door_last_activity_state.state == "2017-03-05T15:03:40+00:00" + + ingress_last_activity_state = hass.states.get("sensor.ingress_last_activity") + assert ingress_last_activity_state.state == "unknown" + + async def test_only_chime_devices( hass: HomeAssistant, requests_mock: requests_mock.Mocker,