From 5f2fd1b0e67c558e1f63a480051e17c1569a284f Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 24 Jan 2022 06:07:06 -0800 Subject: [PATCH] Add a reboot button for ONVIF devices (#61522) --- homeassistant/components/onvif/__init__.py | 2 +- homeassistant/components/onvif/button.py | 41 ++++++ tests/components/onvif/__init__.py | 148 +++++++++++++++++++++ tests/components/onvif/test_button.py | 40 ++++++ tests/components/onvif/test_config_flow.py | 141 +++----------------- 5 files changed, 245 insertions(+), 127 deletions(-) create mode 100644 homeassistant/components/onvif/button.py create mode 100644 tests/components/onvif/test_button.py diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index f6a6eabeb68..044a37edd72 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -83,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.unique_id] = device - platforms = [Platform.CAMERA] + platforms = [Platform.BUTTON, Platform.CAMERA] if device.capabilities.events: platforms += [Platform.BINARY_SENSOR, Platform.SENSOR] diff --git a/homeassistant/components/onvif/button.py b/homeassistant/components/onvif/button.py new file mode 100644 index 00000000000..034573299e6 --- /dev/null +++ b/homeassistant/components/onvif/button.py @@ -0,0 +1,41 @@ +"""ONVIF Buttons.""" + +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .base import ONVIFBaseEntity +from .const import DOMAIN +from .device import ONVIFDevice + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ONVIF button based on a config entry.""" + device = hass.data[DOMAIN][config_entry.unique_id] + async_add_entities([RebootButton(device)]) + + +class RebootButton(ONVIFBaseEntity, ButtonEntity): + """Defines a ONVIF reboot button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = ENTITY_CATEGORY_CONFIG + + def __init__(self, device: ONVIFDevice) -> None: + """Initialize the button entity.""" + super().__init__(device) + self._attr_name = f"{self.device.name} Reboot" + self._attr_unique_id = ( + f"{self.device.info.mac or self.device.info.serial_number}_reboot" + ) + + async def async_press(self) -> None: + """Send out a SystemReboot command.""" + device_mgmt = self.device.device.create_devicemgmt_service() + await device_mgmt.SystemReboot() diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py index 433a6392f12..28413ae4d05 100644 --- a/tests/components/onvif/__init__.py +++ b/tests/components/onvif/__init__.py @@ -1 +1,149 @@ """Tests for the ONVIF integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from zeep.exceptions import Fault + +from homeassistant import config_entries +from homeassistant.components.onvif import config_flow +from homeassistant.components.onvif.const import CONF_SNAPSHOT_AUTH +from homeassistant.components.onvif.models import DeviceInfo +from homeassistant.const import HTTP_DIGEST_AUTHENTICATION + +from tests.common import MockConfigEntry + +URN = "urn:uuid:123456789" +NAME = "TestCamera" +HOST = "1.2.3.4" +PORT = 80 +USERNAME = "admin" +PASSWORD = "12345" +MAC = "aa:bb:cc:dd:ee" +SERIAL_NUMBER = "ABCDEFGHIJK" +MANUFACTURER = "TestManufacturer" +MODEL = "TestModel" +FIRMWARE_VERSION = "TestFirmwareVersion" + + +def setup_mock_onvif_camera( + mock_onvif_camera, + with_h264=True, + two_profiles=False, + with_interfaces=True, + with_interfaces_not_implemented=False, + with_serial=True, +): + """Prepare mock onvif.ONVIFCamera.""" + devicemgmt = MagicMock() + + device_info = MagicMock() + device_info.SerialNumber = SERIAL_NUMBER if with_serial else None + devicemgmt.GetDeviceInformation = AsyncMock(return_value=device_info) + + interface = MagicMock() + interface.Enabled = True + interface.Info.HwAddress = MAC + + if with_interfaces_not_implemented: + devicemgmt.GetNetworkInterfaces = AsyncMock( + side_effect=Fault("not implemented") + ) + else: + devicemgmt.GetNetworkInterfaces = AsyncMock( + return_value=[interface] if with_interfaces else [] + ) + + media_service = MagicMock() + + profile1 = MagicMock() + profile1.VideoEncoderConfiguration.Encoding = "H264" if with_h264 else "MJPEG" + profile2 = MagicMock() + profile2.VideoEncoderConfiguration.Encoding = "H264" if two_profiles else "MJPEG" + + media_service.GetProfiles = AsyncMock(return_value=[profile1, profile2]) + + mock_onvif_camera.update_xaddrs = AsyncMock(return_value=True) + mock_onvif_camera.create_devicemgmt_service = MagicMock(return_value=devicemgmt) + mock_onvif_camera.create_media_service = MagicMock(return_value=media_service) + mock_onvif_camera.close = AsyncMock(return_value=None) + + def mock_constructor( + host, + port, + user, + passwd, + wsdl_dir, + encrypt=True, + no_cache=False, + adjust_time=False, + transport=None, + ): + """Fake the controller constructor.""" + return mock_onvif_camera + + mock_onvif_camera.side_effect = mock_constructor + + +def setup_mock_device(mock_device): + """Prepare mock ONVIFDevice.""" + mock_device.async_setup = AsyncMock(return_value=True) + mock_device.available = True + mock_device.name = NAME + mock_device.info = DeviceInfo( + MANUFACTURER, + MODEL, + FIRMWARE_VERSION, + SERIAL_NUMBER, + MAC, + ) + + def mock_constructor(hass, config): + """Fake the controller constructor.""" + return mock_device + + mock_device.side_effect = mock_constructor + + +async def setup_onvif_integration( + hass, + config=None, + options=None, + unique_id=MAC, + entry_id="1", + source=config_entries.SOURCE_USER, +): + """Create an ONVIF config entry.""" + if not config: + config = { + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + CONF_SNAPSHOT_AUTH: HTTP_DIGEST_AUTHENTICATION, + } + + config_entry = MockConfigEntry( + domain=config_flow.DOMAIN, + source=source, + data={**config}, + options=options or {}, + entry_id=entry_id, + unique_id=unique_id, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device: + setup_mock_onvif_camera(mock_onvif_camera, two_profiles=True) + # no discovery + mock_discovery.return_value = [] + setup_mock_device(mock_device) + mock_device.device = mock_onvif_camera + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry, mock_onvif_camera, mock_device diff --git a/tests/components/onvif/test_button.py b/tests/components/onvif/test_button.py new file mode 100644 index 00000000000..a8ac24da524 --- /dev/null +++ b/tests/components/onvif/test_button.py @@ -0,0 +1,40 @@ +"""Test button of ONVIF integration.""" +from unittest.mock import AsyncMock + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.helpers import entity_registry as er + +from . import MAC, setup_onvif_integration + + +async def test_reboot_button(hass): + """Test states of the Reboot button.""" + await setup_onvif_integration(hass) + + state = hass.states.get("button.testcamera_reboot") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART + + registry = er.async_get(hass) + entry = registry.async_get("button.testcamera_reboot") + assert entry + assert entry.unique_id == f"{MAC}_reboot" + + +async def test_reboot_button_press(hass): + """Test Reboot button press.""" + _, camera, _ = await setup_onvif_integration(hass) + devicemgmt = camera.create_devicemgmt_service() + devicemgmt.SystemReboot = AsyncMock(return_value=True) + + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {ATTR_ENTITY_ID: "button.testcamera_reboot"}, + blocking=True, + ) + await hass.async_block_till_done() + + devicemgmt.SystemReboot.assert_called_once() diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index e4cb079515c..d4a90ce81c6 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -1,5 +1,5 @@ """Test ONVIF config flow.""" -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch from onvif.exceptions import ONVIFError from zeep.exceptions import Fault @@ -7,16 +7,19 @@ from zeep.exceptions import Fault from homeassistant import config_entries, data_entry_flow from homeassistant.components.onvif import config_flow -from tests.common import MockConfigEntry - -URN = "urn:uuid:123456789" -NAME = "TestCamera" -HOST = "1.2.3.4" -PORT = 80 -USERNAME = "admin" -PASSWORD = "12345" -MAC = "aa:bb:cc:dd:ee" -SERIAL_NUMBER = "ABCDEFGHIJK" +from . import ( + HOST, + MAC, + NAME, + PASSWORD, + PORT, + SERIAL_NUMBER, + URN, + USERNAME, + setup_mock_device, + setup_mock_onvif_camera, + setup_onvif_integration, +) DISCOVERY = [ { @@ -36,65 +39,6 @@ DISCOVERY = [ ] -def setup_mock_onvif_camera( - mock_onvif_camera, - with_h264=True, - two_profiles=False, - with_interfaces=True, - with_interfaces_not_implemented=False, - with_serial=True, -): - """Prepare mock onvif.ONVIFCamera.""" - devicemgmt = MagicMock() - - device_info = MagicMock() - device_info.SerialNumber = SERIAL_NUMBER if with_serial else None - devicemgmt.GetDeviceInformation = AsyncMock(return_value=device_info) - - interface = MagicMock() - interface.Enabled = True - interface.Info.HwAddress = MAC - - if with_interfaces_not_implemented: - devicemgmt.GetNetworkInterfaces = AsyncMock( - side_effect=Fault("not implemented") - ) - else: - devicemgmt.GetNetworkInterfaces = AsyncMock( - return_value=[interface] if with_interfaces else [] - ) - - media_service = MagicMock() - - profile1 = MagicMock() - profile1.VideoEncoderConfiguration.Encoding = "H264" if with_h264 else "MJPEG" - profile2 = MagicMock() - profile2.VideoEncoderConfiguration.Encoding = "H264" if two_profiles else "MJPEG" - - media_service.GetProfiles = AsyncMock(return_value=[profile1, profile2]) - - mock_onvif_camera.update_xaddrs = AsyncMock(return_value=True) - mock_onvif_camera.create_devicemgmt_service = MagicMock(return_value=devicemgmt) - mock_onvif_camera.create_media_service = MagicMock(return_value=media_service) - mock_onvif_camera.close = AsyncMock(return_value=None) - - def mock_constructor( - host, - port, - user, - passwd, - wsdl_dir, - encrypt=True, - no_cache=False, - adjust_time=False, - transport=None, - ): - """Fake the controller constructor.""" - return mock_onvif_camera - - mock_onvif_camera.side_effect = mock_constructor - - def setup_mock_discovery( mock_discovery, with_name=False, with_mac=False, two_devices=False ): @@ -126,61 +70,6 @@ def setup_mock_discovery( mock_discovery.return_value = services -def setup_mock_device(mock_device): - """Prepare mock ONVIFDevice.""" - mock_device.async_setup = AsyncMock(return_value=True) - - def mock_constructor(hass, config): - """Fake the controller constructor.""" - return mock_device - - mock_device.side_effect = mock_constructor - - -async def setup_onvif_integration( - hass, - config=None, - options=None, - unique_id=MAC, - entry_id="1", - source=config_entries.SOURCE_USER, -): - """Create an ONVIF config entry.""" - if not config: - config = { - config_flow.CONF_NAME: NAME, - config_flow.CONF_HOST: HOST, - config_flow.CONF_PORT: PORT, - config_flow.CONF_USERNAME: USERNAME, - config_flow.CONF_PASSWORD: PASSWORD, - } - - config_entry = MockConfigEntry( - domain=config_flow.DOMAIN, - source=source, - data={**config}, - options=options or {}, - entry_id=entry_id, - unique_id=unique_id, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.wsdiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: - setup_mock_onvif_camera(mock_onvif_camera, two_profiles=True) - # no discovery - mock_discovery.return_value = [] - setup_mock_device(mock_device) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - return config_entry - - async def test_flow_discovered_devices(hass): """Test that config flow works for discovered devices.""" @@ -616,7 +505,7 @@ async def test_flow_import_onvif_auth_error(hass): async def test_option_flow(hass): """Test config flow options.""" - entry = await setup_onvif_integration(hass) + entry, _, _ = await setup_onvif_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id)