From a9ab98fb4519975636cb5aa29bab26d9b42c83d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 18:10:15 -1000 Subject: [PATCH] Add power sensor to WiZ (#73260) --- homeassistant/components/wiz/__init__.py | 10 ++++-- homeassistant/components/wiz/entity.py | 9 +++-- homeassistant/components/wiz/manifest.json | 2 +- homeassistant/components/wiz/sensor.py | 38 ++++++++++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wiz/__init__.py | 13 ++++++++ tests/components/wiz/test_sensor.py | 30 +++++++++++++++++ 8 files changed, 95 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index 104ecb6f0c5..b47db32d90f 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -1,4 +1,6 @@ """WiZ Platform integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging @@ -80,10 +82,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Found bulb {bulb.mac} at {ip_address}, expected {entry.unique_id}" ) - async def _async_update() -> None: + async def _async_update() -> float | None: """Update the WiZ device.""" try: await bulb.updateState() + if bulb.power_monitoring is not False: + power: float | None = await bulb.get_power() + return power + return None except WIZ_EXCEPTIONS as ex: raise UpdateFailed(f"Failed to update device at {ip_address}: {ex}") from ex @@ -117,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def _async_push_update(state: PilotParser) -> None: """Receive a push update.""" _LOGGER.debug("%s: Got push update: %s", bulb.mac, state.pilotResult) - coordinator.async_set_updated_data(None) + coordinator.async_set_updated_data(coordinator.data) if state.get_source() == PIR_SOURCE: async_dispatcher_send(hass, SIGNAL_WIZ_PIR.format(bulb.mac)) diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py index 9b22d35de7d..c78f3e3b37b 100644 --- a/homeassistant/components/wiz/entity.py +++ b/homeassistant/components/wiz/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from typing import Any +from typing import Any, Optional from pywizlight.bulblibrary import BulbType @@ -10,12 +10,15 @@ from homeassistant.const import ATTR_HW_VERSION, ATTR_MODEL from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .models import WizData -class WizEntity(CoordinatorEntity, Entity): +class WizEntity(CoordinatorEntity[DataUpdateCoordinator[Optional[float]]], Entity): """Representation of WiZ entity.""" def __init__(self, wiz_data: WizData, name: str) -> None: diff --git a/homeassistant/components/wiz/manifest.json b/homeassistant/components/wiz/manifest.json index a3537d3cbd9..c2eb6fe1b53 100644 --- a/homeassistant/components/wiz/manifest.json +++ b/homeassistant/components/wiz/manifest.json @@ -13,7 +13,7 @@ "dependencies": ["network"], "quality_scale": "platinum", "documentation": "https://www.home-assistant.io/integrations/wiz", - "requirements": ["pywizlight==0.5.13"], + "requirements": ["pywizlight==0.5.14"], "iot_class": "local_push", "codeowners": ["@sbidy"] } diff --git a/homeassistant/components/wiz/sensor.py b/homeassistant/components/wiz/sensor.py index d16130883d5..11f3933fd16 100644 --- a/homeassistant/components/wiz/sensor.py +++ b/homeassistant/components/wiz/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.const import POWER_WATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -30,6 +30,17 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ) +POWER_SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="power", + name="Current Power", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_WATT, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -37,9 +48,17 @@ async def async_setup_entry( ) -> None: """Set up the wiz sensor.""" wiz_data: WizData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ WizSensor(wiz_data, entry.title, description) for description in SENSORS - ) + ] + if wiz_data.coordinator.data is not None: + entities.extend( + [ + WizPowerSensor(wiz_data, entry.title, description) + for description in POWER_SENSORS + ] + ) + async_add_entities(entities) class WizSensor(WizEntity, SensorEntity): @@ -63,3 +82,16 @@ class WizSensor(WizEntity, SensorEntity): self._attr_native_value = self._device.state.pilotResult.get( self.entity_description.key ) + + +class WizPowerSensor(WizSensor): + """Defines a WiZ power sensor.""" + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + # Newer firmwares will have the power in their state + watts_push = self._device.state.get_power() + # Older firmwares will be polled and in the coordinator data + watts_poll = self.coordinator.data + self._attr_native_value = watts_poll if watts_push is None else watts_push diff --git a/requirements_all.txt b/requirements_all.txt index b71d68bdaea..5f27d8c524a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2029,7 +2029,7 @@ pywemo==0.9.1 pywilight==0.0.70 # homeassistant.components.wiz -pywizlight==0.5.13 +pywizlight==0.5.14 # homeassistant.components.ws66i pyws66i==1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7ce45f3db3..900b8fda255 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1349,7 +1349,7 @@ pywemo==0.9.1 pywilight==0.0.70 # homeassistant.components.wiz -pywizlight==0.5.13 +pywizlight==0.5.14 # homeassistant.components.ws66i pyws66i==1.1 diff --git a/tests/components/wiz/__init__.py b/tests/components/wiz/__init__.py index 62920662c6f..93033d984fa 100644 --- a/tests/components/wiz/__init__.py +++ b/tests/components/wiz/__init__.py @@ -150,6 +150,17 @@ FAKE_SOCKET = BulbType( white_channels=2, white_to_color_ratio=80, ) +FAKE_SOCKET_WITH_POWER_MONITORING = BulbType( + bulb_type=BulbClass.SOCKET, + name="ESP25_SOCKET_01", + features=Features( + color=False, color_tmp=False, effect=False, brightness=False, dual_head=False + ), + kelvin_range=KelvinRange(2700, 6500), + fw_version="1.26.2", + white_channels=2, + white_to_color_ratio=80, +) FAKE_OLD_FIRMWARE_DIMMABLE_BULB = BulbType( bulb_type=BulbClass.DW, name=None, @@ -197,7 +208,9 @@ def _mocked_wizlight(device, extended_white_range, bulb_type) -> wizlight: ) bulb.getMac = AsyncMock(return_value=FAKE_MAC) bulb.turn_on = AsyncMock() + bulb.get_power = AsyncMock(return_value=None) bulb.turn_off = AsyncMock() + bulb.power_monitoring = False bulb.updateState = AsyncMock(return_value=FAKE_STATE) bulb.getSupportedScenes = AsyncMock(return_value=list(SCENES.values())) bulb.start_push = AsyncMock(side_effect=_save_setup_callback) diff --git a/tests/components/wiz/test_sensor.py b/tests/components/wiz/test_sensor.py index 37a6b04dad3..a1eb6ded51d 100644 --- a/tests/components/wiz/test_sensor.py +++ b/tests/components/wiz/test_sensor.py @@ -1,11 +1,15 @@ """Tests for the sensor platform.""" +from unittest.mock import AsyncMock + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import ( FAKE_DUAL_HEAD_RGBWW_BULB, FAKE_MAC, + FAKE_SOCKET_WITH_POWER_MONITORING, + _mocked_wizlight, _patch_discovery, _patch_wizlight, async_push_update, @@ -35,3 +39,29 @@ async def test_signal_strength(hass: HomeAssistant) -> None: await async_push_update(hass, bulb, {"mac": FAKE_MAC, "rssi": -50}) assert hass.states.get(entity_id).state == "-50" + + +async def test_power_monitoring(hass: HomeAssistant) -> None: + """Test power monitoring.""" + socket = _mocked_wizlight(None, None, FAKE_SOCKET_WITH_POWER_MONITORING) + socket.power_monitoring = None + socket.get_power = AsyncMock(return_value=5.123) + _, entry = await async_setup_integration( + hass, wizlight=socket, bulb_type=FAKE_SOCKET_WITH_POWER_MONITORING + ) + entity_id = "sensor.mock_title_current_power" + entity_registry = er.async_get(hass) + reg_entry = entity_registry.async_get(entity_id) + assert reg_entry.unique_id == f"{FAKE_MAC}_power" + updated_entity = entity_registry.async_update_entity( + entity_id=entity_id, disabled_by=None + ) + assert not updated_entity.disabled + + with _patch_discovery(), _patch_wizlight(device=socket): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "5.123" + await async_push_update(hass, socket, {"mac": FAKE_MAC, "pc": 800}) + assert hass.states.get(entity_id).state == "0.8"