From ca7bd0e339a1127d82bac8fd253ba53ab48a9cd6 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 24 Nov 2021 13:47:38 +0000 Subject: [PATCH 01/11] Add binary sensor platform --- .../devolo_home_network/__init__.py | 17 +++ .../devolo_home_network/binary_sensor.py | 135 ++++++++++++++++++ .../components/devolo_home_network/const.py | 4 +- 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/devolo_home_network/binary_sensor.py diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index f427e5acbfc967..a39f4b881cfc72 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -20,6 +20,7 @@ CONNECTED_PLC_DEVICES, CONNECTED_WIFI_CLIENTS, DOMAIN, + FIRMWARE_UPDATE_AVAILABLE, LONG_UPDATE_INTERVAL, NEIGHBORING_WIFI_NETWORKS, PLATFORMS, @@ -53,6 +54,14 @@ async def async_update_connected_plc_devices() -> dict[str, Any]: except DeviceUnavailable as err: raise UpdateFailed(err) from err + async def async_update_firmware_available() -> dict[str, Any]: + """Fetch data from API endpoint.""" + try: + async with async_timeout.timeout(10): + return await device.device.async_check_firmware_available() # type: ignore[no-any-return, union-attr] + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + async def async_update_wifi_connected_station() -> dict[str, Any]: """Fetch data from API endpoint.""" try: @@ -97,6 +106,14 @@ async def disconnect(event: Event) -> None: update_method=async_update_wifi_neighbor_access_points, update_interval=LONG_UPDATE_INTERVAL, ) + if device.device and "update" in device.device.features: + coordinators[FIRMWARE_UPDATE_AVAILABLE] = DataUpdateCoordinator( + hass, + _LOGGER, + name=FIRMWARE_UPDATE_AVAILABLE, + update_method=async_update_firmware_available, + update_interval=LONG_UPDATE_INTERVAL, + ) hass.data[DOMAIN][entry.entry_id] = {"device": device, "coordinators": coordinators} diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py new file mode 100644 index 00000000000000..3ef7b95438dfc0 --- /dev/null +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -0,0 +1,135 @@ +"""Platform for binary sensor integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from devolo_plc_api.device import Device + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PLUG, + DEVICE_CLASS_UPDATE, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import ( + CONNECTED_PLC_DEVICES, + CONNECTED_TO_ROUTER, + DOMAIN, + FIRMWARE_UPDATE_AVAILABLE, +) +from .entity import DevoloEntity + + +@dataclass +class DevoloBinarySensorRequiredKeysMixin: + """Mixin for required keys.""" + + value_func: Callable[[dict[str, Any]], bool] + + +@dataclass +class DevoloBinarySensorEntityDescription( + BinarySensorEntityDescription, DevoloBinarySensorRequiredKeysMixin +): + """Describes devolo sensor entity.""" + + +SENSOR_TYPES: dict[str, DevoloBinarySensorEntityDescription] = { + FIRMWARE_UPDATE_AVAILABLE: DevoloBinarySensorEntityDescription( + key=FIRMWARE_UPDATE_AVAILABLE, + device_class=DEVICE_CLASS_UPDATE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=True, + icon="mdi:update", + name="Firmware update available", + value_func=lambda data: data["result"] == "UPDATE_AVAILABLE", # type: ignore[no-any-return] + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id][ + "coordinators" + ] + + entities: list[BinarySensorEntity] = [] + if device.device and "update" in device.device.features: + entities.append( + DevoloBinarySensorEntity( + coordinators[FIRMWARE_UPDATE_AVAILABLE], + SENSOR_TYPES[FIRMWARE_UPDATE_AVAILABLE], + device, + entry.title, + ) + ) + if device.plcnet: + entities.append( + DevoloAttachedToRouterEntity( + coordinators[CONNECTED_PLC_DEVICES], + device, + entry.title, + ) + ) + async_add_entities(entities) + + +class DevoloBinarySensorEntity(DevoloEntity, BinarySensorEntity): + """Representation of a devolo binary sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + description: DevoloBinarySensorEntityDescription, + device: Device, + device_name: str, + ) -> None: + """Initialize entity.""" + self.entity_description: DevoloBinarySensorEntityDescription = description + super().__init__(coordinator, device, device_name) + + @property + def is_on(self) -> bool: + """State of the binary sensor.""" + return self.entity_description.value_func(self.coordinator.data) + + +class DevoloAttachedToRouterEntity(DevoloEntity, BinarySensorEntity): + """Representation of a special devolo binary sensor. It is special, because the current value cannot be extracted from coordinator data only.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + device: Device, + device_name: str, + ) -> None: + """Initialize entity.""" + self.entity_description = BinarySensorEntityDescription( + key=CONNECTED_TO_ROUTER, + device_class=DEVICE_CLASS_PLUG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:router-network", + name="Connected to router", + ) + super().__init__(coordinator, device, device_name) + + @property + def is_on(self) -> bool: + """State of the binary sensor.""" + return all( + d["attached_to_router"] + for d in self.coordinator.data["network"]["devices"] + if d["mac_address"] == self._device.mac + ) diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index bd7170bfde570b..0f34cbab47f516 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -5,7 +5,7 @@ from homeassistant.const import Platform DOMAIN = "devolo_home_network" -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] PRODUCT = "product" SERIAL_NUMBER = "serial_number" @@ -15,5 +15,7 @@ SHORT_UPDATE_INTERVAL = timedelta(seconds=15) CONNECTED_PLC_DEVICES = "connected_plc_devices" +CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" +FIRMWARE_UPDATE_AVAILABLE = "firmware_update_available" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" From 46f8438ec634fcea9af3c9dfda6abb616ac282d8 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 24 Nov 2021 16:21:02 +0000 Subject: [PATCH 02/11] Add tests --- .../devolo_home_network/__init__.py | 1 + .../devolo_home_network/conftest.py | 11 +- tests/components/devolo_home_network/const.py | 21 ++- .../devolo_home_network/test_binary_sensor.py | 130 ++++++++++++++++++ 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 tests/components/devolo_home_network/test_binary_sensor.py diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index 913193be3f7570..67439fca86c914 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -28,5 +28,6 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: async def async_connect(self, session_instance: Any = None): """Give a mocked device the needed properties.""" + self.mac = DISCOVERY_INFO["properties"]["PlcMacAddress"] self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) diff --git a/tests/components/devolo_home_network/conftest.py b/tests/components/devolo_home_network/conftest.py index 1d8d2a6da19532..0a8033a12d1f48 100644 --- a/tests/components/devolo_home_network/conftest.py +++ b/tests/components/devolo_home_network/conftest.py @@ -5,7 +5,13 @@ import pytest from . import async_connect -from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NEIGHBOR_ACCESS_POINTS, PLCNET +from .const import ( + CONNECTED_STATIONS, + DISCOVERY_INFO, + FIRMWARE_AVAILABLE, + NEIGHBOR_ACCESS_POINTS, + PLCNET, +) @pytest.fixture() @@ -13,6 +19,9 @@ def mock_device(): """Mock connecting to a devolo home network device.""" with patch("devolo_plc_api.device.Device.async_connect", async_connect), patch( "devolo_plc_api.device.Device.async_disconnect" + ), patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_check_firmware_available", + new=AsyncMock(return_value=FIRMWARE_AVAILABLE), ), patch( "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", new=AsyncMock(return_value=CONNECTED_STATIONS), diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index 0e48833a78b1af..1995bf830a306c 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -47,6 +47,8 @@ type="mock_type", ) +FIRMWARE_AVAILABLE = {"result": "UPDATE_NOT_AVAILABLE", "new_firmware_version": ""} + NEIGHBOR_ACCESS_POINTS = { "neighbor_aps": [ { @@ -62,6 +64,12 @@ PLCNET = { "network": { + "devices": [ + { + "mac_address": "AA:BB:CC:DD:EE:FF", + "attached_to_router": False, + } + ], "data_rates": [ { "mac_address_from": "AA:BB:CC:DD:EE:FF", @@ -70,6 +78,17 @@ "tx_rate": 0.0, }, ], - "devices": [], + } +} + +PLCNET_ATTACHED = { + "network": { + "devices": [ + { + "mac_address": "AA:BB:CC:DD:EE:FF", + "attached_to_router": True, + } + ], + "data_rates": [], } } diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py new file mode 100644 index 00000000000000..6d92eea7131575 --- /dev/null +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -0,0 +1,130 @@ +"""Tests for the devolo Home Network sensors.""" +from unittest.mock import AsyncMock, patch + +from devolo_plc_api.exceptions.device import DeviceUnavailable +import pytest + +from homeassistant.components.binary_sensor import DOMAIN +from homeassistant.components.devolo_home_network.const import ( + CONNECTED_TO_ROUTER, + FIRMWARE_UPDATE_AVAILABLE, + LONG_UPDATE_INTERVAL, +) +from homeassistant.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.util import dt + +from . import configure_integration +from .const import PLCNET_ATTACHED + +from tests.common import async_fire_time_changed + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_binary_sensor_setup(hass: HomeAssistant): + """Test default setup of the binary sensor component.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{DOMAIN}.{FIRMWARE_UPDATE_AVAILABLE}") is not None + assert hass.states.get(f"{DOMAIN}.{CONNECTED_TO_ROUTER}") is None + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_update_firmware_update_available(hass: HomeAssistant): + """Test state change of a firmware_update_available binary sensor device.""" + state_key = f"{DOMAIN}.{FIRMWARE_UPDATE_AVAILABLE}" + + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + + er = entity_registry.async_get(hass) + assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC + + # Emulate device failure + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_check_firmware_available", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Emulate state change + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_check_firmware_available", + new=AsyncMock(return_value={"result": "UPDATE_AVAILABLE"}), + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_update_attached_to_router(hass: HomeAssistant): + """Test state change of a attached_to_router binary sensor device.""" + state_key = f"{DOMAIN}.{CONNECTED_TO_ROUTER}" + entry = configure_integration(hass) + with patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + return_value=True, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + + er = entity_registry.async_get(hass) + assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC + + # Emulate device failure + with patch( + "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Emulate state change + with patch( + "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", + new=AsyncMock(return_value=PLCNET_ATTACHED), + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + + await hass.config_entries.async_unload(entry.entry_id) From ae4cc31497be712159913fa8ab99db6d8399320a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 25 Nov 2021 19:34:05 +0000 Subject: [PATCH 03/11] Switch to single DevoloBinarySensorEntity --- .../devolo_home_network/binary_sensor.py | 53 +++++++------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 3ef7b95438dfc0..60b0747da3af7a 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -3,7 +3,6 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Any from devolo_plc_api.device import Device @@ -32,7 +31,7 @@ class DevoloBinarySensorRequiredKeysMixin: """Mixin for required keys.""" - value_func: Callable[[dict[str, Any]], bool] + value_func: Callable[[DevoloBinarySensorEntity], bool] @dataclass @@ -43,6 +42,19 @@ class DevoloBinarySensorEntityDescription( SENSOR_TYPES: dict[str, DevoloBinarySensorEntityDescription] = { + CONNECTED_TO_ROUTER: DevoloBinarySensorEntityDescription( + key=CONNECTED_TO_ROUTER, + device_class=DEVICE_CLASS_PLUG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:router-network", + name="Connected to router", + value_func=lambda entity: all( + device["attached_to_router"] + for device in entity.coordinator.data["network"]["devices"] + if device["mac_address"] == entity._device.mac + ), + ), FIRMWARE_UPDATE_AVAILABLE: DevoloBinarySensorEntityDescription( key=FIRMWARE_UPDATE_AVAILABLE, device_class=DEVICE_CLASS_UPDATE, @@ -50,7 +62,7 @@ class DevoloBinarySensorEntityDescription( entity_registry_enabled_default=True, icon="mdi:update", name="Firmware update available", - value_func=lambda data: data["result"] == "UPDATE_AVAILABLE", # type: ignore[no-any-return] + value_func=lambda entity: entity.coordinator.data["result"] == "UPDATE_AVAILABLE", # type: ignore[no-any-return] ), } @@ -76,8 +88,9 @@ async def async_setup_entry( ) if device.plcnet: entities.append( - DevoloAttachedToRouterEntity( + DevoloBinarySensorEntity( coordinators[CONNECTED_PLC_DEVICES], + SENSOR_TYPES[CONNECTED_TO_ROUTER], device, entry.title, ) @@ -102,34 +115,4 @@ def __init__( @property def is_on(self) -> bool: """State of the binary sensor.""" - return self.entity_description.value_func(self.coordinator.data) - - -class DevoloAttachedToRouterEntity(DevoloEntity, BinarySensorEntity): - """Representation of a special devolo binary sensor. It is special, because the current value cannot be extracted from coordinator data only.""" - - def __init__( - self, - coordinator: DataUpdateCoordinator, - device: Device, - device_name: str, - ) -> None: - """Initialize entity.""" - self.entity_description = BinarySensorEntityDescription( - key=CONNECTED_TO_ROUTER, - device_class=DEVICE_CLASS_PLUG, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - entity_registry_enabled_default=False, - icon="mdi:router-network", - name="Connected to router", - ) - super().__init__(coordinator, device, device_name) - - @property - def is_on(self) -> bool: - """State of the binary sensor.""" - return all( - d["attached_to_router"] - for d in self.coordinator.data["network"]["devices"] - if d["mac_address"] == self._device.mac - ) + return self.entity_description.value_func(self) From 6053e265a6ad0bf3fc85a2ccb7db995463b0c691 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 25 Nov 2021 19:40:36 +0000 Subject: [PATCH 04/11] Fix pylint --- .../devolo_home_network/binary_sensor.py | 2 +- .../components/devolo_home_network/entity.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 60b0747da3af7a..5bb9c4c3cff442 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -52,7 +52,7 @@ class DevoloBinarySensorEntityDescription( value_func=lambda entity: all( device["attached_to_router"] for device in entity.coordinator.data["network"]["devices"] - if device["mac_address"] == entity._device.mac + if device["mac_address"] == entity.device.mac ), ), FIRMWARE_UPDATE_AVAILABLE: DevoloBinarySensorEntityDescription( diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index dbfe0e4035a127..56227b0fc1d056 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -21,17 +21,17 @@ def __init__( """Initialize a devolo home network device.""" super().__init__(coordinator) - self._device = device - self._device_name = device_name + self.device = device + self.device_name = device_name self._attr_device_info = DeviceInfo( - configuration_url=f"http://{self._device.ip}", - identifiers={(DOMAIN, str(self._device.serial_number))}, + configuration_url=f"http://{self.device.ip}", + identifiers={(DOMAIN, str(self.device.serial_number))}, manufacturer="devolo", - model=self._device.product, - name=self._device_name, - sw_version=self._device.firmware_version, + model=self.device.product, + name=self.device_name, + sw_version=self.device.firmware_version, ) self._attr_unique_id = ( - f"{self._device.serial_number}_{self.entity_description.key}" + f"{self.device.serial_number}_{self.entity_description.key}" ) From c0b51def1a0321c61eb9c8b386079c024d207b55 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 25 Nov 2021 19:43:59 +0000 Subject: [PATCH 05/11] Replace lamba by function --- .../devolo_home_network/binary_sensor.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 5bb9c4c3cff442..0377e51ca09286 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -27,6 +27,15 @@ from .entity import DevoloEntity +def _is_connected_to_router(entity: DevoloBinarySensorEntity) -> bool: + """Check, if device is attached to the router.""" + return all( + device["attached_to_router"] + for device in entity.coordinator.data["network"]["devices"] + if device["mac_address"] == entity.device.mac + ) + + @dataclass class DevoloBinarySensorRequiredKeysMixin: """Mixin for required keys.""" @@ -49,11 +58,7 @@ class DevoloBinarySensorEntityDescription( entity_registry_enabled_default=False, icon="mdi:router-network", name="Connected to router", - value_func=lambda entity: all( - device["attached_to_router"] - for device in entity.coordinator.data["network"]["devices"] - if device["mac_address"] == entity.device.mac - ), + value_func=_is_connected_to_router, ), FIRMWARE_UPDATE_AVAILABLE: DevoloBinarySensorEntityDescription( key=FIRMWARE_UPDATE_AVAILABLE, From a5a90a35c6c60d5ea72206e74ddc83a7c1691592 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 25 Nov 2021 21:56:45 +0000 Subject: [PATCH 06/11] Combine decorators --- .../components/devolo_home_network/test_binary_sensor.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 6d92eea7131575..ba23dbbd2b0520 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -26,8 +26,7 @@ from tests.common import async_fire_time_changed -@pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") +@pytest.mark.usefixtures("mock_device", "mock_zeroconf") async def test_binary_sensor_setup(hass: HomeAssistant): """Test default setup of the binary sensor component.""" entry = configure_integration(hass) @@ -40,8 +39,7 @@ async def test_binary_sensor_setup(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") +@pytest.mark.usefixtures("mock_device", "mock_zeroconf") async def test_update_firmware_update_available(hass: HomeAssistant): """Test state change of a firmware_update_available binary sensor device.""" state_key = f"{DOMAIN}.{FIRMWARE_UPDATE_AVAILABLE}" @@ -84,8 +82,7 @@ async def test_update_firmware_update_available(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") +@pytest.mark.usefixtures("mock_device", "mock_zeroconf") async def test_update_attached_to_router(hass: HomeAssistant): """Test state change of a attached_to_router binary sensor device.""" state_key = f"{DOMAIN}.{CONNECTED_TO_ROUTER}" From 8e79d4e956991213fd3ca584c0766729261bbd8a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 26 Nov 2021 08:35:00 +0000 Subject: [PATCH 07/11] Update entity init --- .../components/devolo_home_network/entity.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index 56227b0fc1d056..dd26324bc2c33c 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -22,16 +22,13 @@ def __init__( super().__init__(coordinator) self.device = device - self.device_name = device_name self._attr_device_info = DeviceInfo( - configuration_url=f"http://{self.device.ip}", - identifiers={(DOMAIN, str(self.device.serial_number))}, + configuration_url=f"http://{device.ip}", + identifiers={(DOMAIN, str(device.serial_number))}, manufacturer="devolo", - model=self.device.product, - name=self.device_name, - sw_version=self.device.firmware_version, - ) - self._attr_unique_id = ( - f"{self.device.serial_number}_{self.entity_description.key}" + model=device.product, + name=device_name, + sw_version=device.firmware_version, ) + self._attr_unique_id = f"{device.serial_number}_{self.entity_description.key}" From 1fc0f091ca9340aee00f49dc9d859de33c013919 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 7 Dec 2021 11:16:55 +0000 Subject: [PATCH 08/11] Apply suggestions --- .../devolo_home_network/__init__.py | 2 +- .../devolo_home_network/test_binary_sensor.py | 71 ++++++++++--------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index 67439fca86c914..1c10d7a59ef6bf 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -28,6 +28,6 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: async def async_connect(self, session_instance: Any = None): """Give a mocked device the needed properties.""" - self.mac = DISCOVERY_INFO["properties"]["PlcMacAddress"] + self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index ba23dbbd2b0520..35d41e3df36bb6 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -87,41 +87,46 @@ async def test_update_attached_to_router(hass: HomeAssistant): """Test state change of a attached_to_router binary sensor device.""" state_key = f"{DOMAIN}.{CONNECTED_TO_ROUTER}" entry = configure_integration(hass) + + er = entity_registry.async_get(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Enable entity + er.async_update_entity(state_key, disabled_by=None) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + + assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC + + # Emulate device failure with patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - return_value=True, + "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Emulate state change + with patch( + "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", + new=AsyncMock(return_value=PLCNET_ATTACHED), ): - await hass.config_entries.async_setup(entry.entry_id) + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) await hass.async_block_till_done() + state = hass.states.get(state_key) assert state is not None - assert state.state == STATE_OFF - - er = entity_registry.async_get(hass) - assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC - - # Emulate device failure - with patch( - "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", - side_effect=DeviceUnavailable, - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE - - # Emulate state change - with patch( - "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", - new=AsyncMock(return_value=PLCNET_ATTACHED), - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_ON - - await hass.config_entries.async_unload(entry.entry_id) + assert state.state == STATE_ON + + await hass.config_entries.async_unload(entry.entry_id) From e52ab0fd3298ada6c9aac7657f8726eced945c6d Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 25 Apr 2022 20:52:57 +0000 Subject: [PATCH 09/11] Remove update sensor --- .../devolo_home_network/binary_sensor.py | 30 +--------- .../devolo_home_network/test_binary_sensor.py | 55 +------------------ 2 files changed, 6 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 0377e51ca09286..6bc02d802f5b79 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -8,22 +8,16 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PLUG, - DEVICE_CLASS_UPDATE, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ( - CONNECTED_PLC_DEVICES, - CONNECTED_TO_ROUTER, - DOMAIN, - FIRMWARE_UPDATE_AVAILABLE, -) +from .const import CONNECTED_PLC_DEVICES, CONNECTED_TO_ROUTER, DOMAIN from .entity import DevoloEntity @@ -54,21 +48,12 @@ class DevoloBinarySensorEntityDescription( CONNECTED_TO_ROUTER: DevoloBinarySensorEntityDescription( key=CONNECTED_TO_ROUTER, device_class=DEVICE_CLASS_PLUG, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:router-network", name="Connected to router", value_func=_is_connected_to_router, ), - FIRMWARE_UPDATE_AVAILABLE: DevoloBinarySensorEntityDescription( - key=FIRMWARE_UPDATE_AVAILABLE, - device_class=DEVICE_CLASS_UPDATE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - entity_registry_enabled_default=True, - icon="mdi:update", - name="Firmware update available", - value_func=lambda entity: entity.coordinator.data["result"] == "UPDATE_AVAILABLE", # type: ignore[no-any-return] - ), } @@ -82,15 +67,6 @@ async def async_setup_entry( ] entities: list[BinarySensorEntity] = [] - if device.device and "update" in device.device.features: - entities.append( - DevoloBinarySensorEntity( - coordinators[FIRMWARE_UPDATE_AVAILABLE], - SENSOR_TYPES[FIRMWARE_UPDATE_AVAILABLE], - device, - entry.title, - ) - ) if device.plcnet: entities.append( DevoloBinarySensorEntity( diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 35d41e3df36bb6..3d181da6106d7f 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -7,17 +7,12 @@ from homeassistant.components.binary_sensor import DOMAIN from homeassistant.components.devolo_home_network.const import ( CONNECTED_TO_ROUTER, - FIRMWARE_UPDATE_AVAILABLE, LONG_UPDATE_INTERVAL, ) -from homeassistant.const import ( - ENTITY_CATEGORY_DIAGNOSTIC, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt from . import configure_integration @@ -33,55 +28,11 @@ async def test_binary_sensor_setup(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.{FIRMWARE_UPDATE_AVAILABLE}") is not None assert hass.states.get(f"{DOMAIN}.{CONNECTED_TO_ROUTER}") is None await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("mock_device", "mock_zeroconf") -async def test_update_firmware_update_available(hass: HomeAssistant): - """Test state change of a firmware_update_available binary sensor device.""" - state_key = f"{DOMAIN}.{FIRMWARE_UPDATE_AVAILABLE}" - - entry = configure_integration(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_OFF - - er = entity_registry.async_get(hass) - assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC - - # Emulate device failure - with patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_check_firmware_available", - side_effect=DeviceUnavailable, - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE - - # Emulate state change - with patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_check_firmware_available", - new=AsyncMock(return_value={"result": "UPDATE_AVAILABLE"}), - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_ON - - await hass.config_entries.async_unload(entry.entry_id) - - @pytest.mark.usefixtures("mock_device", "mock_zeroconf") async def test_update_attached_to_router(hass: HomeAssistant): """Test state change of a attached_to_router binary sensor device.""" @@ -103,7 +54,7 @@ async def test_update_attached_to_router(hass: HomeAssistant): assert state is not None assert state.state == STATE_OFF - assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert er.async_get(state_key).entity_category == EntityCategory.DIAGNOSTIC # Emulate device failure with patch( From beaa9c1c63aa2e036a1d4a0e899d5f135cf7b7c5 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 25 Apr 2022 20:59:29 +0000 Subject: [PATCH 10/11] Remove fwupdate coordinator --- .../components/devolo_home_network/__init__.py | 17 ----------------- .../components/devolo_home_network/const.py | 1 - 2 files changed, 18 deletions(-) diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index a39f4b881cfc72..f427e5acbfc967 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -20,7 +20,6 @@ CONNECTED_PLC_DEVICES, CONNECTED_WIFI_CLIENTS, DOMAIN, - FIRMWARE_UPDATE_AVAILABLE, LONG_UPDATE_INTERVAL, NEIGHBORING_WIFI_NETWORKS, PLATFORMS, @@ -54,14 +53,6 @@ async def async_update_connected_plc_devices() -> dict[str, Any]: except DeviceUnavailable as err: raise UpdateFailed(err) from err - async def async_update_firmware_available() -> dict[str, Any]: - """Fetch data from API endpoint.""" - try: - async with async_timeout.timeout(10): - return await device.device.async_check_firmware_available() # type: ignore[no-any-return, union-attr] - except DeviceUnavailable as err: - raise UpdateFailed(err) from err - async def async_update_wifi_connected_station() -> dict[str, Any]: """Fetch data from API endpoint.""" try: @@ -106,14 +97,6 @@ async def disconnect(event: Event) -> None: update_method=async_update_wifi_neighbor_access_points, update_interval=LONG_UPDATE_INTERVAL, ) - if device.device and "update" in device.device.features: - coordinators[FIRMWARE_UPDATE_AVAILABLE] = DataUpdateCoordinator( - hass, - _LOGGER, - name=FIRMWARE_UPDATE_AVAILABLE, - update_method=async_update_firmware_available, - update_interval=LONG_UPDATE_INTERVAL, - ) hass.data[DOMAIN][entry.entry_id] = {"device": device, "coordinators": coordinators} diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index 0f34cbab47f516..2dfdd3c1d9ae7d 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -17,5 +17,4 @@ CONNECTED_PLC_DEVICES = "connected_plc_devices" CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" -FIRMWARE_UPDATE_AVAILABLE = "firmware_update_available" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" From 78cd487ffabe2fc826039c59a3b275895926426a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 25 Apr 2022 21:05:10 +0000 Subject: [PATCH 11/11] Remove unused test code --- tests/components/devolo_home_network/conftest.py | 11 +---------- tests/components/devolo_home_network/const.py | 2 -- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/components/devolo_home_network/conftest.py b/tests/components/devolo_home_network/conftest.py index 0a8033a12d1f48..1d8d2a6da19532 100644 --- a/tests/components/devolo_home_network/conftest.py +++ b/tests/components/devolo_home_network/conftest.py @@ -5,13 +5,7 @@ import pytest from . import async_connect -from .const import ( - CONNECTED_STATIONS, - DISCOVERY_INFO, - FIRMWARE_AVAILABLE, - NEIGHBOR_ACCESS_POINTS, - PLCNET, -) +from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NEIGHBOR_ACCESS_POINTS, PLCNET @pytest.fixture() @@ -19,9 +13,6 @@ def mock_device(): """Mock connecting to a devolo home network device.""" with patch("devolo_plc_api.device.Device.async_connect", async_connect), patch( "devolo_plc_api.device.Device.async_disconnect" - ), patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_check_firmware_available", - new=AsyncMock(return_value=FIRMWARE_AVAILABLE), ), patch( "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", new=AsyncMock(return_value=CONNECTED_STATIONS), diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index 1995bf830a306c..516a19f3421ed1 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -47,8 +47,6 @@ type="mock_type", ) -FIRMWARE_AVAILABLE = {"result": "UPDATE_NOT_AVAILABLE", "new_firmware_version": ""} - NEIGHBOR_ACCESS_POINTS = { "neighbor_aps": [ {