Skip to content

Commit

Permalink
Reolink Chime online status and ability to remove (#123301)
Browse files Browse the repository at this point in the history
* Add chime available

* allow removing a Reolink chime

* Allow removal if doorbell itself removed

* fix tests

* Add tests

* fix styling
  • Loading branch information
starkillerOG authored Aug 8, 2024
1 parent 634a2b2 commit 2343f5e
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 27 deletions.
40 changes: 40 additions & 0 deletions homeassistant/components/reolink/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,46 @@ async def async_remove_config_entry_device(
host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host
(device_uid, ch, is_chime) = get_device_uid_and_ch(device, host)

if is_chime:
await host.api.get_state(cmd="GetDingDongList")
chime = host.api.chime(ch)
if (
chime is None
or chime.connect_state is None
or chime.connect_state < 0
or chime.channel not in host.api.channels
):
_LOGGER.debug(
"Removing Reolink chime %s with id %s, "
"since it is not coupled to %s anymore",
device.name,
ch,
host.api.nvr_name,
)
return True

# remove the chime from the host
await chime.remove()
await host.api.get_state(cmd="GetDingDongList")
if chime.connect_state < 0:
_LOGGER.debug(
"Removed Reolink chime %s with id %s from %s",
device.name,
ch,
host.api.nvr_name,
)
return True

_LOGGER.warning(
"Cannot remove Reolink chime %s with id %s, because it is still connected "
"to %s, please first remove the chime "
"in the reolink app",
device.name,
ch,
host.api.nvr_name,
)
return False

if not host.api.is_nvr or ch is None:
_LOGGER.warning(
"Cannot remove Reolink device %s, because it is not a camera connected "
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/reolink/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,8 @@ def __init__(
serial_number=str(chime.dev_id),
configuration_url=self._conf_url,
)

@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._chime.online and super().available
32 changes: 32 additions & 0 deletions tests/components/reolink/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from reolink_aio.api import Chime

from homeassistant.components.reolink import const
from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL
Expand Down Expand Up @@ -107,6 +108,14 @@ def reolink_connect_class() -> Generator[MagicMock]:
host_mock.capabilities = {"Host": ["RTSP"], "0": ["motion_detection"]}
host_mock.checked_api_versions = {"GetEvents": 1}
host_mock.abilities = {"abilityChn": [{"aiTrack": {"permit": 0, "ver": 0}}]}

# enums
host_mock.whiteled_mode.return_value = 1
host_mock.whiteled_mode_list.return_value = ["off", "auto"]
host_mock.doorbell_led.return_value = "Off"
host_mock.doorbell_led_list.return_value = ["stayoff", "auto"]
host_mock.auto_track_method.return_value = 3
host_mock.daynight_state.return_value = "Black&White"
yield host_mock_class


Expand Down Expand Up @@ -145,3 +154,26 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry:
)
config_entry.add_to_hass(hass)
return config_entry


@pytest.fixture
def test_chime(reolink_connect: MagicMock) -> None:
"""Mock a reolink chime."""
TEST_CHIME = Chime(
host=reolink_connect,
dev_id=12345678,
channel=0,
)
TEST_CHIME.name = "Test chime"
TEST_CHIME.volume = 3
TEST_CHIME.connect_state = 2
TEST_CHIME.led_state = True
TEST_CHIME.event_info = {
"md": {"switch": 0, "musicId": 0},
"people": {"switch": 0, "musicId": 1},
"visitor": {"switch": 1, "musicId": 2},
}

reolink_connect.chime_list = [TEST_CHIME]
reolink_connect.chime.return_value = TEST_CHIME
return TEST_CHIME
80 changes: 76 additions & 4 deletions tests/components/reolink/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest
from reolink_aio.api import Chime
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError

from homeassistant.components.reolink import (
Expand Down Expand Up @@ -40,6 +41,8 @@

pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms")

CHIME_MODEL = "Reolink Chime"


async def test_wait(*args, **key_args):
"""Ensure a mocked function takes a bit of time to be able to timeout in test."""
Expand Down Expand Up @@ -224,16 +227,85 @@ async def test_removing_disconnected_cams(
device_models = [device.model for device in device_entries]
assert sorted(device_models) == sorted([TEST_HOST_MODEL, TEST_CAM_MODEL])

# reload integration after 'disconnecting' a camera.
# Try to remove the device after 'disconnecting' a camera.
if attr is not None:
setattr(reolink_connect, attr, value)
expected_success = TEST_CAM_MODEL not in expected_models
for device in device_entries:
if device.model == TEST_CAM_MODEL:
response = await client.remove_device(device.id, config_entry.entry_id)
assert response["success"] == expected_success

device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
device_models = [device.model for device in device_entries]
assert sorted(device_models) == sorted(expected_models)


@pytest.mark.parametrize(
("attr", "value", "expected_models"),
[
(
None,
None,
[TEST_HOST_MODEL, TEST_CAM_MODEL, CHIME_MODEL],
),
(
"connect_state",
-1,
[TEST_HOST_MODEL, TEST_CAM_MODEL],
),
(
"remove",
-1,
[TEST_HOST_MODEL, TEST_CAM_MODEL],
),
],
)
async def test_removing_chime(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
config_entry: MockConfigEntry,
reolink_connect: MagicMock,
test_chime: Chime,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
attr: str | None,
value: Any,
expected_models: list[str],
) -> None:
"""Test removing a chime."""
reolink_connect.channels = [0]
assert await async_setup_component(hass, "config", {})
client = await hass_ws_client(hass)
# setup CH 0 and NVR switch entities/device
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
assert await hass.config_entries.async_reload(config_entry.entry_id)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

expected_success = TEST_CAM_MODEL not in expected_models
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
device_models = [device.model for device in device_entries]
assert sorted(device_models) == sorted(
[TEST_HOST_MODEL, TEST_CAM_MODEL, CHIME_MODEL]
)

if attr == "remove":

async def test_remove_chime(*args, **key_args):
"""Remove chime."""
test_chime.connect_state = -1

test_chime.remove = test_remove_chime
elif attr is not None:
setattr(test_chime, attr, value)

# Try to remove the device after 'disconnecting' a chime.
expected_success = CHIME_MODEL not in expected_models
for device in device_entries:
if device.model == TEST_CAM_MODEL:
if device.model == CHIME_MODEL:
response = await client.remove_device(device.id, config_entry.entry_id)
assert response["success"] == expected_success

Expand Down
37 changes: 14 additions & 23 deletions tests/components/reolink/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ async def test_floodlight_mode_select(
entity_registry: er.EntityRegistry,
) -> None:
"""Test select entity with floodlight_mode."""
reolink_connect.whiteled_mode.return_value = 1
reolink_connect.whiteled_mode_list.return_value = ["off", "auto"]
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
await hass.async_block_till_done()
Expand Down Expand Up @@ -72,6 +70,14 @@ async def test_floodlight_mode_select(
blocking=True,
)

reolink_connect.whiteled_mode.return_value = -99 # invalid value
async_fire_time_changed(
hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30)
)
await hass.async_block_till_done()

assert hass.states.is_state(entity_id, STATE_UNKNOWN)


async def test_play_quick_reply_message(
hass: HomeAssistant,
Expand Down Expand Up @@ -103,25 +109,10 @@ async def test_chime_select(
hass: HomeAssistant,
config_entry: MockConfigEntry,
reolink_connect: MagicMock,
test_chime: Chime,
entity_registry: er.EntityRegistry,
) -> None:
"""Test chime select entity."""
TEST_CHIME = Chime(
host=reolink_connect,
dev_id=12345678,
channel=0,
)
TEST_CHIME.name = "Test chime"
TEST_CHIME.volume = 3
TEST_CHIME.led_state = True
TEST_CHIME.event_info = {
"md": {"switch": 0, "musicId": 0},
"people": {"switch": 0, "musicId": 1},
"visitor": {"switch": 1, "musicId": 2},
}

reolink_connect.chime_list = [TEST_CHIME]

with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
await hass.async_block_till_done()
Expand All @@ -131,16 +122,16 @@ async def test_chime_select(
entity_id = f"{Platform.SELECT}.test_chime_visitor_ringtone"
assert hass.states.is_state(entity_id, "pianokey")

TEST_CHIME.set_tone = AsyncMock()
test_chime.set_tone = AsyncMock()
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, "option": "off"},
blocking=True,
)
TEST_CHIME.set_tone.assert_called_once()
test_chime.set_tone.assert_called_once()

TEST_CHIME.set_tone = AsyncMock(side_effect=ReolinkError("Test error"))
test_chime.set_tone = AsyncMock(side_effect=ReolinkError("Test error"))
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
SELECT_DOMAIN,
Expand All @@ -149,7 +140,7 @@ async def test_chime_select(
blocking=True,
)

TEST_CHIME.set_tone = AsyncMock(side_effect=InvalidParameterError("Test error"))
test_chime.set_tone = AsyncMock(side_effect=InvalidParameterError("Test error"))
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
SELECT_DOMAIN,
Expand All @@ -158,7 +149,7 @@ async def test_chime_select(
blocking=True,
)

TEST_CHIME.event_info = {}
test_chime.event_info = {}
async_fire_time_changed(
hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30)
)
Expand Down

0 comments on commit 2343f5e

Please sign in to comment.