mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
Reolink Chime online status and ability to remove (#123301)
* Add chime available * allow removing a Reolink chime * Allow removal if doorbell itself removed * fix tests * Add tests * fix styling
This commit is contained in:
parent
634a2b22dc
commit
2343f5e40f
@ -188,6 +188,46 @@ async def async_remove_config_entry_device(
|
|||||||
host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host
|
host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host
|
||||||
(device_uid, ch, is_chime) = get_device_uid_and_ch(device, 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:
|
if not host.api.is_nvr or ch is None:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Cannot remove Reolink device %s, because it is not a camera connected "
|
"Cannot remove Reolink device %s, because it is not a camera connected "
|
||||||
|
@ -190,3 +190,8 @@ class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity):
|
|||||||
serial_number=str(chime.dev_id),
|
serial_number=str(chime.dev_id),
|
||||||
configuration_url=self._conf_url,
|
configuration_url=self._conf_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self._chime.online and super().available
|
||||||
|
@ -4,6 +4,7 @@ from collections.abc import Generator
|
|||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from reolink_aio.api import Chime
|
||||||
|
|
||||||
from homeassistant.components.reolink import const
|
from homeassistant.components.reolink import const
|
||||||
from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL
|
from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL
|
||||||
@ -107,6 +108,14 @@ def reolink_connect_class() -> Generator[MagicMock]:
|
|||||||
host_mock.capabilities = {"Host": ["RTSP"], "0": ["motion_detection"]}
|
host_mock.capabilities = {"Host": ["RTSP"], "0": ["motion_detection"]}
|
||||||
host_mock.checked_api_versions = {"GetEvents": 1}
|
host_mock.checked_api_versions = {"GetEvents": 1}
|
||||||
host_mock.abilities = {"abilityChn": [{"aiTrack": {"permit": 0, "ver": 0}}]}
|
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
|
yield host_mock_class
|
||||||
|
|
||||||
|
|
||||||
@ -145,3 +154,26 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
|||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
return config_entry
|
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
|
||||||
|
@ -6,6 +6,7 @@ from typing import Any
|
|||||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from reolink_aio.api import Chime
|
||||||
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
|
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
|
||||||
|
|
||||||
from homeassistant.components.reolink import (
|
from homeassistant.components.reolink import (
|
||||||
@ -40,6 +41,8 @@ from tests.typing import WebSocketGenerator
|
|||||||
|
|
||||||
pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms")
|
pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms")
|
||||||
|
|
||||||
|
CHIME_MODEL = "Reolink Chime"
|
||||||
|
|
||||||
|
|
||||||
async def test_wait(*args, **key_args):
|
async def test_wait(*args, **key_args):
|
||||||
"""Ensure a mocked function takes a bit of time to be able to timeout in test."""
|
"""Ensure a mocked function takes a bit of time to be able to timeout in test."""
|
||||||
@ -224,13 +227,9 @@ async def test_removing_disconnected_cams(
|
|||||||
device_models = [device.model for device in device_entries]
|
device_models = [device.model for device in device_entries]
|
||||||
assert sorted(device_models) == sorted([TEST_HOST_MODEL, TEST_CAM_MODEL])
|
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:
|
if attr is not None:
|
||||||
setattr(reolink_connect, attr, value)
|
setattr(reolink_connect, attr, value)
|
||||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
|
|
||||||
assert await hass.config_entries.async_reload(config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
expected_success = TEST_CAM_MODEL not in expected_models
|
expected_success = TEST_CAM_MODEL not in expected_models
|
||||||
for device in device_entries:
|
for device in device_entries:
|
||||||
if device.model == TEST_CAM_MODEL:
|
if device.model == TEST_CAM_MODEL:
|
||||||
@ -244,6 +243,79 @@ async def test_removing_disconnected_cams(
|
|||||||
assert sorted(device_models) == sorted(expected_models)
|
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_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
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 == CHIME_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(
|
@pytest.mark.parametrize(
|
||||||
(
|
(
|
||||||
"original_id",
|
"original_id",
|
||||||
|
@ -33,8 +33,6 @@ async def test_floodlight_mode_select(
|
|||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test select entity with floodlight_mode."""
|
"""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]):
|
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
|
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -72,6 +70,14 @@ async def test_floodlight_mode_select(
|
|||||||
blocking=True,
|
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(
|
async def test_play_quick_reply_message(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -103,25 +109,10 @@ async def test_chime_select(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
reolink_connect: MagicMock,
|
reolink_connect: MagicMock,
|
||||||
|
test_chime: Chime,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test chime select entity."""
|
"""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]):
|
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
|
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -131,16 +122,16 @@ async def test_chime_select(
|
|||||||
entity_id = f"{Platform.SELECT}.test_chime_visitor_ringtone"
|
entity_id = f"{Platform.SELECT}.test_chime_visitor_ringtone"
|
||||||
assert hass.states.is_state(entity_id, "pianokey")
|
assert hass.states.is_state(entity_id, "pianokey")
|
||||||
|
|
||||||
TEST_CHIME.set_tone = AsyncMock()
|
test_chime.set_tone = AsyncMock()
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
{ATTR_ENTITY_ID: entity_id, "option": "off"},
|
{ATTR_ENTITY_ID: entity_id, "option": "off"},
|
||||||
blocking=True,
|
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):
|
with pytest.raises(HomeAssistantError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
@ -149,7 +140,7 @@ async def test_chime_select(
|
|||||||
blocking=True,
|
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):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
@ -158,7 +149,7 @@ async def test_chime_select(
|
|||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
TEST_CHIME.event_info = {}
|
test_chime.event_info = {}
|
||||||
async_fire_time_changed(
|
async_fire_time_changed(
|
||||||
hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30)
|
hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30)
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user