Skip to content

Commit

Permalink
Allow setting volume on Ring devices (#125773)
Browse files Browse the repository at this point in the history
* Turn Ring Doorbell and Chime volumes into number entities.

* turn RingOther volumes into numbers as well

* fix linter issues

* move other volume strings into `number` section

* add back old volume sensors but deprecate them

* add tests for `ring.number`

* add back strings for sensors that have just become deprecated

* remove deprecated volume sensors from test

* Revert "remove deprecated volume sensors from test"

This reverts commit fc95af6.

* create entities for deprecated sensors so that tests still run

* remove print

* add entities immediately

* move `RingNumberEntityDescription` above `RingNumber` and remove unused import

* remove irrelevant comment about history

* fix not using `setter_fn`

* add missing icons for other volume entities

* rename `entity` -> `entity_id` in number tests

* fix typing in number test

* use constants for `hass.services.async_call()`

* use `@refresh_after` decorator instead of delaying updates manually

* move descriptors above entity class

* Use snapshot to test states.

* add missing snapshot file for number platform

* Update homeassistant/components/ring/number.py

Co-authored-by: Steven B. <[email protected]>

---------

Co-authored-by: Steven B. <[email protected]>
  • Loading branch information
daniel-k and sdb9696 authored Sep 17, 2024
1 parent 9557386 commit c8e2408
Show file tree
Hide file tree
Showing 9 changed files with 2,687 additions and 6 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/ring/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Platform.CAMERA,
Platform.EVENT,
Platform.LIGHT,
Platform.NUMBER,
Platform.SENSOR,
Platform.SIREN,
Platform.SWITCH,
Expand Down
14 changes: 14 additions & 0 deletions homeassistant/components/ring/icons.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
{
"entity": {
"number": {
"volume": {
"default": "mdi:bell-ring"
},
"doorbell_volume": {
"default": "mdi:bell-ring"
},
"mic_volume": {
"default": "mdi:microphone"
},
"voice_volume": {
"default": "mdi:account-voice"
}
},
"sensor": {
"last_activity": {
"default": "mdi:history"
Expand Down
150 changes: 150 additions & 0 deletions homeassistant/components/ring/number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""Component providing HA number support for Ring Door Bell/Chimes."""

from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, Generic, cast

from ring_doorbell import RingChime, RingDoorBell, RingGeneric, RingOther
import ring_doorbell.const

from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType

from . import RingConfigEntry
from .coordinator import RingDataCoordinator
from .entity import RingDeviceT, RingEntity, refresh_after


async def async_setup_entry(
hass: HomeAssistant,
entry: RingConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a numbers for a Ring device."""
ring_data = entry.runtime_data
devices_coordinator = ring_data.devices_coordinator

async_add_entities(
RingNumber(device, devices_coordinator, description)
for description in NUMBER_TYPES
for device in ring_data.devices.all_devices
if description.exists_fn(device)
)


@dataclass(frozen=True, kw_only=True)
class RingNumberEntityDescription(NumberEntityDescription, Generic[RingDeviceT]):
"""Describes Ring number entity."""

value_fn: Callable[[RingDeviceT], StateType]
setter_fn: Callable[[RingDeviceT, float], Awaitable[None]]
exists_fn: Callable[[RingGeneric], bool]


NUMBER_TYPES: tuple[RingNumberEntityDescription[Any], ...] = (
RingNumberEntityDescription[RingChime](
key="volume",
translation_key="volume",
mode=NumberMode.SLIDER,
native_min_value=ring_doorbell.const.CHIME_VOL_MIN,
native_max_value=ring_doorbell.const.CHIME_VOL_MAX,
native_step=1,
value_fn=lambda device: device.volume,
setter_fn=lambda device, value: device.async_set_volume(int(value)),
exists_fn=lambda device: isinstance(device, RingChime),
),
RingNumberEntityDescription[RingDoorBell](
key="volume",
translation_key="volume",
mode=NumberMode.SLIDER,
native_min_value=ring_doorbell.const.DOORBELL_VOL_MIN,
native_max_value=ring_doorbell.const.DOORBELL_VOL_MAX,
native_step=1,
value_fn=lambda device: device.volume,
setter_fn=lambda device, value: device.async_set_volume(int(value)),
exists_fn=lambda device: isinstance(device, RingDoorBell),
),
RingNumberEntityDescription[RingOther](
key="doorbell_volume",
translation_key="doorbell_volume",
mode=NumberMode.SLIDER,
native_min_value=ring_doorbell.const.OTHER_DOORBELL_VOL_MIN,
native_max_value=ring_doorbell.const.OTHER_DOORBELL_VOL_MAX,
native_step=1,
value_fn=lambda device: device.doorbell_volume,
setter_fn=lambda device, value: device.async_set_doorbell_volume(int(value)),
exists_fn=lambda device: isinstance(device, RingOther),
),
RingNumberEntityDescription[RingOther](
key="mic_volume",
translation_key="mic_volume",
mode=NumberMode.SLIDER,
native_min_value=ring_doorbell.const.MIC_VOL_MIN,
native_max_value=ring_doorbell.const.MIC_VOL_MAX,
native_step=1,
value_fn=lambda device: device.mic_volume,
setter_fn=lambda device, value: device.async_set_mic_volume(int(value)),
exists_fn=lambda device: isinstance(device, RingOther),
),
RingNumberEntityDescription[RingOther](
key="voice_volume",
translation_key="voice_volume",
mode=NumberMode.SLIDER,
native_min_value=ring_doorbell.const.VOICE_VOL_MIN,
native_max_value=ring_doorbell.const.VOICE_VOL_MAX,
native_step=1,
value_fn=lambda device: device.voice_volume,
setter_fn=lambda device, value: device.async_set_voice_volume(int(value)),
exists_fn=lambda device: isinstance(device, RingOther),
),
)


class RingNumber(RingEntity[RingDeviceT], NumberEntity):
"""A number implementation for Ring device."""

entity_description: RingNumberEntityDescription[RingDeviceT]

def __init__(
self,
device: RingDeviceT,
coordinator: RingDataCoordinator,
description: RingNumberEntityDescription[RingDeviceT],
) -> None:
"""Initialize a number for Ring device."""
super().__init__(device, coordinator)
self.entity_description = description
self._attr_unique_id = f"{device.id}-{description.key}"
self._update_native_value()

def _update_native_value(self) -> None:
native_value = self.entity_description.value_fn(self._device)
if native_value is not None:
self._attr_native_value = float(native_value)

@callback
def _handle_coordinator_update(self) -> None:
"""Call update method."""

self._device = cast(
RingDeviceT,
self._get_coordinator_data().get_device(self._device.device_api_id),
)

self._update_native_value()

super()._handle_coordinator_update()

@refresh_after
async def async_set_native_value(self, value: float) -> None:
"""Call setter on Ring device."""
await self.entity_description.setter_fn(self._device, value)

self._attr_native_value = value
self.async_write_ha_state()
12 changes: 12 additions & 0 deletions homeassistant/components/ring/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,24 +215,36 @@ class RingSensorEntityDescription(
translation_key="volume",
value_fn=lambda device: device.volume,
exists_fn=lambda device: isinstance(device, (RingDoorBell, RingChime)),
deprecated_info=DeprecatedInfo(
new_platform=Platform.NUMBER, breaks_in_ha_version="2025.4.0"
),
),
RingSensorEntityDescription[RingOther](
key="doorbell_volume",
translation_key="doorbell_volume",
value_fn=lambda device: device.doorbell_volume,
exists_fn=lambda device: isinstance(device, RingOther),
deprecated_info=DeprecatedInfo(
new_platform=Platform.NUMBER, breaks_in_ha_version="2025.4.0"
),
),
RingSensorEntityDescription[RingOther](
key="mic_volume",
translation_key="mic_volume",
value_fn=lambda device: device.mic_volume,
exists_fn=lambda device: isinstance(device, RingOther),
deprecated_info=DeprecatedInfo(
new_platform=Platform.NUMBER, breaks_in_ha_version="2025.4.0"
),
),
RingSensorEntityDescription[RingOther](
key="voice_volume",
translation_key="voice_volume",
value_fn=lambda device: device.voice_volume,
exists_fn=lambda device: isinstance(device, RingOther),
deprecated_info=DeprecatedInfo(
new_platform=Platform.NUMBER, breaks_in_ha_version="2025.4.0"
),
),
RingSensorEntityDescription[RingGeneric](
key="wifi_signal_category",
Expand Down
14 changes: 14 additions & 0 deletions homeassistant/components/ring/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@
"name": "[%key:component::light::title%]"
}
},
"number": {
"volume": {
"name": "Volume"
},
"doorbell_volume": {
"name": "Doorbell volume"
},
"mic_volume": {
"name": "Mic volume"
},
"voice_volume": {
"name": "Voice volume"
}
},
"siren": {
"siren": {
"name": "[%key:component::siren::title%]"
Expand Down
18 changes: 13 additions & 5 deletions tests/components/ring/device_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

from datetime import datetime
from functools import partial
from unittest.mock import AsyncMock, MagicMock

from ring_doorbell import (
Expand Down Expand Up @@ -153,6 +154,9 @@ def update_history_data(fixture):
"doorbell_volume", device_dict["settings"].get("volume")
)
)
mock_device.async_set_volume.side_effect = lambda i: mock_device.configure_mock(
volume=i
)

if has_capability(RingCapability.SIREN):
mock_device.configure_mock(
Expand All @@ -170,10 +174,14 @@ def update_history_data(fixture):
)

if device_family == "other":
mock_device.configure_mock(
doorbell_volume=device_dict["settings"].get("doorbell_volume"),
mic_volume=device_dict["settings"].get("mic_volume"),
voice_volume=device_dict["settings"].get("voice_volume"),
)
for prop in ("doorbell_volume", "mic_volume", "voice_volume"):
mock_device.configure_mock(
**{
prop: device_dict["settings"].get(prop),
f"async_set_{prop}.side_effect": partial(
setattr, mock_device, prop
),
}
)

return mock_device
Loading

0 comments on commit c8e2408

Please sign in to comment.