Use device properties for WeMo Insight sensors (#63525)
parent
349060be2f
commit
c6ba987995
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue