diff --git a/.coveragerc b/.coveragerc index 54430c5de96..49fd1a80c02 100644 --- a/.coveragerc +++ b/.coveragerc @@ -225,7 +225,6 @@ omit = homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py - homeassistant/components/devolo_home_control/sensor.py homeassistant/components/devolo_home_control/switch.py homeassistant/components/digital_ocean/* homeassistant/components/discogs/sensor.py diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 33a17929f71..5009dbb8563 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -169,9 +169,6 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): device_instance.consumption_property[element_uid], f"{consumption}_unit" ) - if consumption == "total": - self._attr_state_class = SensorStateClass.TOTAL_INCREASING - self._value = getattr( device_instance.consumption_property[element_uid], consumption ) diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index 129c4a377ef..0fc01d61841 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -11,6 +11,7 @@ from devolo_home_control_api.properties.binary_sensor_property import ( from devolo_home_control_api.properties.binary_switch_property import ( BinarySwitchProperty, ) +from devolo_home_control_api.properties.consumption_property import ConsumptionProperty from devolo_home_control_api.properties.multi_level_sensor_property import ( MultiLevelSensorProperty, ) @@ -43,6 +44,19 @@ class BinarySwitchPropertyMock(BinarySwitchProperty): self.element_uid = "Test" +class ConsumptionPropertyMock(ConsumptionProperty): + """devolo Home Control binary sensor mock.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + self._logger = MagicMock() + self.element_uid = "devolo.Meter:Test" + self.current_unit = "W" + self.total_unit = "kWh" + self._current = 0.0 + self._total = 0.0 + + class MultiLevelSensorPropertyMock(MultiLevelSensorProperty): """devolo Home Control multi level sensor mock.""" @@ -135,6 +149,19 @@ class ClimateMock(DeviceMock): self.multi_level_sensor_property = {"Test": MultiLevelSensorPropertyMock()} +class ConsumptionMock(DeviceMock): + """devolo Home Control meter device mock.""" + + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + + self.consumption_property = {"devolo.Meter:Test": ConsumptionPropertyMock()} + self.multi_level_sensor_property = { + "devolo.VoltageMultiLevelSensor:Test": MultiLevelSensorPropertyMock() + } + + class CoverMock(DeviceMock): """devolo Home Control cover device mock.""" @@ -195,6 +222,17 @@ class SirenMock(DeviceMock): self.settings_property["tone"] = SettingsMock() +class SensorMock(DeviceMock): + """devolo Home Control sensor device mock.""" + + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.multi_level_sensor_property = { + "devolo.MultiLevelSensor:Test": MultiLevelSensorPropertyMock() + } + + class HomeControlMock(HomeControl): """devolo Home Control gateway mock.""" @@ -203,7 +241,7 @@ class HomeControlMock(HomeControl): self.devices = {} self.publisher = MagicMock() - def websocket_disconnect(self, event: str): + def websocket_disconnect(self, event: str = "") -> None: """Mock disconnect of the websocket.""" @@ -234,6 +272,19 @@ class HomeControlMockClimate(HomeControlMock): self.publisher.unregister = MagicMock() +class HomeControlMockConsumption(HomeControlMock): + """devolo Home Control gateway mock with meter devices.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = { + "Test": ConsumptionMock(), + } + self.publisher = Publisher(self.devices.keys()) + self.publisher.unregister = MagicMock() + + class HomeControlMockCover(HomeControlMock): """devolo Home Control gateway mock with cover devices.""" @@ -280,6 +331,19 @@ class HomeControlMockDisabledBinarySensor(HomeControlMock): self.devices = {"Test": DisabledBinarySensorMock()} +class HomeControlMockSensor(HomeControlMock): + """devolo Home Control gateway mock with sensor devices.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = { + "Test": SensorMock(), + } + self.publisher = Publisher(self.devices.keys()) + self.publisher.unregister = MagicMock() + + class HomeControlMockSiren(HomeControlMock): """devolo Home Control gateway mock with siren device.""" diff --git a/tests/components/devolo_home_control/test_sensor.py b/tests/components/devolo_home_control/test_sensor.py new file mode 100644 index 00000000000..dc4dab9c7f5 --- /dev/null +++ b/tests/components/devolo_home_control/test_sensor.py @@ -0,0 +1,170 @@ +"""Tests for the devolo Home Control sensor platform.""" +from unittest.mock import patch + +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory + +from . import configure_integration +from .mocks import HomeControlMock, HomeControlMockConsumption, HomeControlMockSensor + + +async def test_temperature_sensor(hass: HomeAssistant): + """Test setup of a temperature sensor device.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockSensor() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == str( + test_gateway.devices["Test"] + .multi_level_sensor_property["devolo.MultiLevelSensor:Test"] + .value + ) + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + + +async def test_battery_sensor(hass: HomeAssistant): + """Test setup and state change of a battery sensor device.""" + entry = configure_integration(hass) + er = entity_registry.async_get(hass) + test_gateway = HomeControlMockSensor() + test_gateway.devices["Test"].battery_level = 25 + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test_2") + assert state is not None + assert state.state == str(test_gateway.devices["Test"].battery_level) + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY + assert er.async_get(f"{DOMAIN}.test_2").entity_category is EntityCategory.DIAGNOSTIC + + # Emulate websocket message: value changed + test_gateway.publisher.dispatch("Test", ("Test", 10, "battery_level")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test_2").state == "10" + + +async def test_consumption_sensor(hass: HomeAssistant): + """Test setup and state change of a consumption sensor device.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockConsumption() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test_current") + assert state is not None + assert state.state == str( + test_gateway.devices["Test"].consumption_property["devolo.Meter:Test"].current + ) + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER + + state = hass.states.get(f"{DOMAIN}.test_total") + assert state is not None + assert state.state == str( + test_gateway.devices["Test"].consumption_property["devolo.Meter:Test"].total + ) + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY + + # Emulate websocket message: value changed + test_gateway.devices["Test"].consumption_property["devolo.Meter:Test"].total = 50.0 + test_gateway.publisher.dispatch("Test", ("devolo.Meter:Test", 50.0)) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test_total").state == "50.0" + + # Emulate websocket message: device went offline + test_gateway.devices["Test"].status = 1 + test_gateway.publisher.dispatch("Test", ("Status", False, "status")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test_current").state == STATE_UNAVAILABLE + assert hass.states.get(f"{DOMAIN}.test_total").state == STATE_UNAVAILABLE + + +async def test_voltage_sensor(hass: HomeAssistant): + """Test disabled setup of a voltage sensor device.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockConsumption() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is None + + +async def test_sensor_change(hass: HomeAssistant): + """Test state change of a sensor device.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockSensor() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Emulate websocket message: value changed + test_gateway.publisher.dispatch("Test", ("devolo.MultiLevelSensor:Test", 50.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == "50.0" + + # Emulate websocket message: device went offline + test_gateway.devices["Test"].status = 1 + test_gateway.publisher.dispatch("Test", ("Status", False, "status")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + + +async def test_remove_from_hass(hass: HomeAssistant): + """Test removing entity.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockSensor() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert test_gateway.publisher.unregister.call_count == 1