Use device properties for WeMo Insight sensors (#63525)
This commit is contained in:
parent
349060be2f
commit
c6ba987995
3 changed files with 79 additions and 79 deletions
|
@ -1,5 +1,10 @@
|
||||||
"""Support for power sensors in WeMo Insight devices."""
|
"""Support for power sensors in WeMo Insight devices."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
|
@ -13,13 +18,42 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.util import convert
|
|
||||||
|
|
||||||
from .const import DOMAIN as WEMO_DOMAIN
|
from .const import DOMAIN as WEMO_DOMAIN
|
||||||
from .entity import WemoEntity
|
from .entity import WemoEntity
|
||||||
from .wemo_device import DeviceCoordinator
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
|
@ -30,7 +64,9 @@ async def async_setup_entry(
|
||||||
async def _discovered_wemo(coordinator: DeviceCoordinator) -> None:
|
async def _discovered_wemo(coordinator: DeviceCoordinator) -> None:
|
||||||
"""Handle a discovered Wemo device."""
|
"""Handle a discovered Wemo device."""
|
||||||
async_add_entities(
|
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)
|
async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo)
|
||||||
|
@ -43,66 +79,35 @@ async def async_setup_entry(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InsightSensor(WemoEntity, SensorEntity):
|
class AttributeSensor(WemoEntity, SensorEntity):
|
||||||
"""Common base for WeMo Insight power sensors."""
|
"""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
|
@property
|
||||||
def name_suffix(self) -> str:
|
def name_suffix(self) -> str | None:
|
||||||
"""Return the name of the entity if any."""
|
"""Return the name of the entity."""
|
||||||
assert self.entity_description.name
|
|
||||||
return self.entity_description.name
|
return self.entity_description.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id_suffix(self) -> str:
|
def unique_id_suffix(self) -> str | None:
|
||||||
"""Return the id of this entity."""
|
"""Suffix to append to the WeMo device's unique ID."""
|
||||||
return self.entity_description.key
|
return self.entity_description.unique_id_suffix
|
||||||
|
|
||||||
@property
|
def convert_state(self, value: StateType) -> StateType:
|
||||||
def available(self) -> bool:
|
"""Convert native state to a value appropriate for the sensor."""
|
||||||
"""Return true if sensor is available."""
|
if (convert := self.entity_description.state_conversion) is None:
|
||||||
return (
|
return None
|
||||||
self.entity_description.key in self.wemo.insight_params
|
return convert(value)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the current power consumption."""
|
"""Return the value of the device attribute."""
|
||||||
milliwatts = convert(
|
return self.convert_state(getattr(self.wemo, self.entity_description.key))
|
||||||
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)
|
|
||||||
|
|
|
@ -15,6 +15,9 @@ MOCK_PORT = 50000
|
||||||
MOCK_NAME = "WemoDeviceName"
|
MOCK_NAME = "WemoDeviceName"
|
||||||
MOCK_SERIAL_NUMBER = "WemoSerialNumber"
|
MOCK_SERIAL_NUMBER = "WemoSerialNumber"
|
||||||
MOCK_FIRMWARE_VERSION = "WeMo_WW_2.00.XXXXX.PVT-OWRT"
|
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")
|
@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.get_state.return_value = 0 # Default to Off
|
||||||
device.supports_long_press.return_value = cls.supports_long_press()
|
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"
|
url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml"
|
||||||
with patch("pywemo.setup_url_for_address", return_value=url), patch(
|
with patch("pywemo.setup_url_for_address", return_value=url), patch(
|
||||||
"pywemo.discovery.device_from_description", return_value=device
|
"pywemo.discovery.device_from_description", return_value=device
|
||||||
|
|
|
@ -2,13 +2,7 @@
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.homeassistant import (
|
from .conftest import MOCK_INSIGHT_CURRENT_WATTS, MOCK_INSIGHT_TODAY_KWH
|
||||||
DOMAIN as HA_DOMAIN,
|
|
||||||
SERVICE_UPDATE_ENTITY,
|
|
||||||
)
|
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
|
|
||||||
from homeassistant.setup import async_setup_component
|
|
||||||
|
|
||||||
from .entity_test_helpers import EntityTestHelpers
|
from .entity_test_helpers import EntityTestHelpers
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +32,6 @@ class InsightTestTemplate(EntityTestHelpers):
|
||||||
|
|
||||||
ENTITY_ID_SUFFIX: str
|
ENTITY_ID_SUFFIX: str
|
||||||
EXPECTED_STATE_VALUE: str
|
EXPECTED_STATE_VALUE: str
|
||||||
INSIGHT_PARAM_NAME: str
|
|
||||||
|
|
||||||
@pytest.fixture(name="wemo_entity_suffix")
|
@pytest.fixture(name="wemo_entity_suffix")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -46,30 +39,20 @@ class InsightTestTemplate(EntityTestHelpers):
|
||||||
"""Select the appropriate entity for the test."""
|
"""Select the appropriate entity for the test."""
|
||||||
return cls.ENTITY_ID_SUFFIX
|
return cls.ENTITY_ID_SUFFIX
|
||||||
|
|
||||||
async def test_state_unavailable(self, hass, wemo_entity, pywemo_device):
|
def test_state(self, hass, wemo_entity):
|
||||||
"""Test that there is no failure if the insight_params is not populated."""
|
"""Test the sensor state."""
|
||||||
del pywemo_device.insight_params[self.INSIGHT_PARAM_NAME]
|
assert hass.states.get(wemo_entity.entity_id).state == self.EXPECTED_STATE_VALUE
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class TestInsightCurrentPower(InsightTestTemplate):
|
class TestInsightCurrentPower(InsightTestTemplate):
|
||||||
"""Test the InsightCurrentPower class."""
|
"""Test the InsightCurrentPower class."""
|
||||||
|
|
||||||
ENTITY_ID_SUFFIX = "_current_power"
|
ENTITY_ID_SUFFIX = "_current_power"
|
||||||
EXPECTED_STATE_VALUE = "0.001"
|
EXPECTED_STATE_VALUE = str(MOCK_INSIGHT_CURRENT_WATTS)
|
||||||
INSIGHT_PARAM_NAME = "currentpower"
|
|
||||||
|
|
||||||
|
|
||||||
class TestInsightTodayEnergy(InsightTestTemplate):
|
class TestInsightTodayEnergy(InsightTestTemplate):
|
||||||
"""Test the InsightTodayEnergy class."""
|
"""Test the InsightTodayEnergy class."""
|
||||||
|
|
||||||
ENTITY_ID_SUFFIX = "_today_energy"
|
ENTITY_ID_SUFFIX = "_today_energy"
|
||||||
EXPECTED_STATE_VALUE = "3.33"
|
EXPECTED_STATE_VALUE = str(MOCK_INSIGHT_TODAY_KWH)
|
||||||
INSIGHT_PARAM_NAME = "todaymw"
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue