From c6ba987995e4c726614ffbde2b31ce01a034aab3 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 28 Mar 2022 17:47:18 -0700 Subject: [PATCH] Use device properties for WeMo Insight sensors (#63525) --- homeassistant/components/wemo/sensor.py | 117 ++++++++++++------------ tests/components/wemo/conftest.py | 12 +++ tests/components/wemo/test_sensor.py | 29 ++---- 3 files changed, 79 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index eed5c510936..b79ce08a1e0 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -1,5 +1,10 @@ """Support for power sensors in WeMo Insight devices.""" +from __future__ import annotations + import asyncio +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,13 +18,42 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoEntity from .wemo_device import DeviceCoordinator +@dataclass +class AttributeSensorDescription(SensorEntityDescription): + """SensorEntityDescription for WeMo AttributeSensor entities.""" + + state_conversion: Callable[[StateType], StateType] | None = None + unique_id_suffix: str | None = None + + +ATTRIBUTE_SENSORS = ( + AttributeSensorDescription( + name="Current Power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + key="current_power_watts", + unique_id_suffix="currentpower", + state_conversion=lambda state: round(cast(float, state), 2), + ), + AttributeSensorDescription( + name="Today Energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + key="today_kwh", + unique_id_suffix="todaymw", + state_conversion=lambda state: round(cast(float, state), 2), + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -30,7 +64,9 @@ async def async_setup_entry( async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities( - [InsightCurrentPower(coordinator), InsightTodayEnergy(coordinator)] + AttributeSensor(coordinator, description) + for description in ATTRIBUTE_SENSORS + if hasattr(coordinator.wemo, description.key) ) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo) @@ -43,66 +79,35 @@ async def async_setup_entry( ) -class InsightSensor(WemoEntity, SensorEntity): - """Common base for WeMo Insight power sensors.""" +class AttributeSensor(WemoEntity, SensorEntity): + """Sensor that reads attributes of a wemo device.""" + + entity_description: AttributeSensorDescription + + def __init__( + self, coordinator: DeviceCoordinator, description: AttributeSensorDescription + ) -> None: + """Init AttributeSensor.""" + super().__init__(coordinator) + self.entity_description = description @property - def name_suffix(self) -> str: - """Return the name of the entity if any.""" - assert self.entity_description.name + def name_suffix(self) -> str | None: + """Return the name of the entity.""" return self.entity_description.name @property - def unique_id_suffix(self) -> str: - """Return the id of this entity.""" - return self.entity_description.key + def unique_id_suffix(self) -> str | None: + """Suffix to append to the WeMo device's unique ID.""" + return self.entity_description.unique_id_suffix - @property - def available(self) -> bool: - """Return true if sensor is available.""" - return ( - self.entity_description.key in self.wemo.insight_params - and super().available - ) - - -class InsightCurrentPower(InsightSensor): - """Current instantaineous power consumption.""" - - entity_description = SensorEntityDescription( - key="currentpower", - name="Current Power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, - ) + def convert_state(self, value: StateType) -> StateType: + """Convert native state to a value appropriate for the sensor.""" + if (convert := self.entity_description.state_conversion) is None: + return None + return convert(value) @property def native_value(self) -> StateType: - """Return the current power consumption.""" - milliwatts = convert( - self.wemo.insight_params.get(self.entity_description.key), float, 0.0 - ) - assert isinstance(milliwatts, float) - return milliwatts / 1000.0 - - -class InsightTodayEnergy(InsightSensor): - """Energy used today.""" - - entity_description = SensorEntityDescription( - key="todaymw", - name="Today Energy", - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - ) - - @property - def native_value(self) -> StateType: - """Return the current energy use today.""" - milliwatt_seconds = convert( - self.wemo.insight_params.get(self.entity_description.key), float, 0.0 - ) - assert isinstance(milliwatt_seconds, float) - return round(milliwatt_seconds / (1000.0 * 1000.0 * 60), 2) + """Return the value of the device attribute.""" + return self.convert_state(getattr(self.wemo, self.entity_description.key)) diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index cf974f523a8..af7002a2500 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -15,6 +15,9 @@ MOCK_PORT = 50000 MOCK_NAME = "WemoDeviceName" MOCK_SERIAL_NUMBER = "WemoSerialNumber" MOCK_FIRMWARE_VERSION = "WeMo_WW_2.00.XXXXX.PVT-OWRT" +MOCK_INSIGHT_CURRENT_WATTS = 0.01 +MOCK_INSIGHT_TODAY_KWH = 3.33 +MOCK_INSIGHT_STATE_THRESHOLD_POWER = 8.0 @pytest.fixture(name="pywemo_model") @@ -64,6 +67,15 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model): device.get_state.return_value = 0 # Default to Off device.supports_long_press.return_value = cls.supports_long_press() + if issubclass(cls, pywemo.Insight): + device.get_standby_state = pywemo.StandbyState.OFF + device.current_power_watts = MOCK_INSIGHT_CURRENT_WATTS + device.today_kwh = MOCK_INSIGHT_TODAY_KWH + device.threshold_power_watts = MOCK_INSIGHT_STATE_THRESHOLD_POWER + device.on_for = 1234 + device.today_on_time = 5678 + device.total_on_time = 9012 + url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml" with patch("pywemo.setup_url_for_address", return_value=url), patch( "pywemo.discovery.device_from_description", return_value=device diff --git a/tests/components/wemo/test_sensor.py b/tests/components/wemo/test_sensor.py index 305aad6102c..ab6753975f1 100644 --- a/tests/components/wemo/test_sensor.py +++ b/tests/components/wemo/test_sensor.py @@ -2,13 +2,7 @@ import pytest -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE -from homeassistant.setup import async_setup_component - +from .conftest import MOCK_INSIGHT_CURRENT_WATTS, MOCK_INSIGHT_TODAY_KWH from .entity_test_helpers import EntityTestHelpers @@ -38,7 +32,6 @@ class InsightTestTemplate(EntityTestHelpers): ENTITY_ID_SUFFIX: str EXPECTED_STATE_VALUE: str - INSIGHT_PARAM_NAME: str @pytest.fixture(name="wemo_entity_suffix") @classmethod @@ -46,30 +39,20 @@ class InsightTestTemplate(EntityTestHelpers): """Select the appropriate entity for the test.""" return cls.ENTITY_ID_SUFFIX - async def test_state_unavailable(self, hass, wemo_entity, pywemo_device): - """Test that there is no failure if the insight_params is not populated.""" - del pywemo_device.insight_params[self.INSIGHT_PARAM_NAME] - await async_setup_component(hass, HA_DOMAIN, {}) - await hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, - blocking=True, - ) - assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE + def test_state(self, hass, wemo_entity): + """Test the sensor state.""" + assert hass.states.get(wemo_entity.entity_id).state == self.EXPECTED_STATE_VALUE class TestInsightCurrentPower(InsightTestTemplate): """Test the InsightCurrentPower class.""" ENTITY_ID_SUFFIX = "_current_power" - EXPECTED_STATE_VALUE = "0.001" - INSIGHT_PARAM_NAME = "currentpower" + EXPECTED_STATE_VALUE = str(MOCK_INSIGHT_CURRENT_WATTS) class TestInsightTodayEnergy(InsightTestTemplate): """Test the InsightTodayEnergy class.""" ENTITY_ID_SUFFIX = "_today_energy" - EXPECTED_STATE_VALUE = "3.33" - INSIGHT_PARAM_NAME = "todaymw" + EXPECTED_STATE_VALUE = str(MOCK_INSIGHT_TODAY_KWH)