From c81ebb8aa5c5884173418ffcec099c3001de8cc0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Nov 2019 12:13:59 +0100 Subject: [PATCH 1/2] Add sensor platform to WLED integration --- homeassistant/components/wled/__init__.py | 4 +- homeassistant/components/wled/const.py | 7 ++ homeassistant/components/wled/sensor.py | 132 ++++++++++++++++++++++ tests/components/wled/test_sensor.py | 54 +++++++++ 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/wled/sensor.py create mode 100644 tests/components/wled/test_sensor.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 054c09eb971929..8566db49e58f47 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -7,6 +7,7 @@ from wled import WLED, WLEDConnectionError, WLEDError from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST @@ -60,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} # Set up all platforms for this device/entry. - for component in LIGHT_DOMAIN, SWITCH_DOMAIN: + for component in LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -95,6 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather( hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN), hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), + hass.config_entries.async_forward_entry_unload(entry, SENSOR_DOMAIN), ) # Cleanup diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 0836c801632d92..ff2fd291262bd1 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -14,7 +14,9 @@ ATTR_FADE = "fade" ATTR_IDENTIFIERS = "identifiers" ATTR_INTENSITY = "intensity" +ATTR_LED_COUNT = "led_count" ATTR_MANUFACTURER = "manufacturer" +ATTR_MAX_POWER = "max_power" ATTR_MODEL = "model" ATTR_ON = "on" ATTR_PALETTE = "palette" @@ -25,3 +27,8 @@ ATTR_SPEED = "speed" ATTR_TARGET_BRIGHTNESS = "target_brightness" ATTR_UDP_PORT = "udp_port" + +# Units of measurement +CURRENT_MA = "mA" +DATA_BYTES = "bytes" +TIME_SECONDS = "seconds" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py new file mode 100644 index 00000000000000..e3faf2c108887a --- /dev/null +++ b/homeassistant/components/wled/sensor.py @@ -0,0 +1,132 @@ +"""Support for WLED sensors.""" +import logging +from typing import Callable, List, Union + +from homeassistant.components.wled import WLED, WLEDDeviceEntity +from homeassistant.components.wled.const import ( + ATTR_LED_COUNT, + ATTR_MAX_POWER, + CURRENT_MA, + DATA_BYTES, + DATA_WLED_CLIENT, + DOMAIN, + TIME_SECONDS, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up WLED sensor based on a config entry.""" + wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + + sensors = [ + WLEDEstimatedCurrentSensor(entry.entry_id, wled), + WLEDUptimeSensor(entry.entry_id, wled), + WLEDFreeHeapSensor(entry.entry_id, wled), + ] + + async_add_entities(sensors, True) + + +class WLEDSensor(WLEDDeviceEntity): + """Defines a WLED sensor.""" + + def __init__( + self, + entry_id: str, + wled: WLED, + name: str, + icon: str, + unit_of_measurement: str, + key: str, + ) -> None: + """Initialize WLED sensor.""" + self._state = None + self._unit_of_measurement = unit_of_measurement + self._key = key + + super().__init__(entry_id, wled, name, icon) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self.wled.device.info.mac_address}_{self._key}" + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + +class WLEDEstimatedCurrentSensor(WLEDSensor): + """Defines a WLED estimated current sensor.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED estimated current sensor.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Estimated Current", + "mdi:power", + CURRENT_MA, + "estimated_current", + ) + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._state = self.wled.device.info.leds.power + self._attributes = { + ATTR_LED_COUNT: self.wled.device.info.leds.count, + ATTR_MAX_POWER: self.wled.device.info.leds.max_power, + } + + +class WLEDUptimeSensor(WLEDSensor): + """Defines a WLED uptime sensor.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED uptime sensor.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Uptime", + "mdi:clock-outline", + TIME_SECONDS, + "uptime", + ) + + async def _wled_update(self) -> None: + """Update WLED uptime sensor.""" + self._state = self.wled.device.info.uptime + + +class WLEDFreeHeapSensor(WLEDSensor): + """Defines a WLED free heap sensor.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED free heap sensor.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Free Memory", + "mdi:memory", + DATA_BYTES, + "free_heap", + ) + + async def _wled_update(self) -> None: + """Update WLED uptime sensor.""" + self._state = self.wled.device.info.free_heap diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py new file mode 100644 index 00000000000000..f1b092a783651c --- /dev/null +++ b/tests/components/wled/test_sensor.py @@ -0,0 +1,54 @@ +"""Tests for the WLED sensor platform.""" +from homeassistant.components.wled.const import ( + ATTR_LED_COUNT, + ATTR_MAX_POWER, + CURRENT_MA, + DATA_BYTES, + TIME_SECONDS, +) +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant + +from tests.components.wled import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the WLED sensors.""" + await init_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.wled_rgb_light_estimated_current") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:power" + assert state.attributes.get(ATTR_LED_COUNT) == 30 + assert state.attributes.get(ATTR_MAX_POWER) == 850 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENT_MA + assert state.state == "470" + + entry = entity_registry.async_get("sensor.wled_rgb_light_estimated_current") + assert entry + assert entry.unique_id == "aabbccddeeff_estimated_current" + + state = hass.states.get("sensor.wled_rgb_light_uptime") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_SECONDS + assert state.state == "32" + + entry = entity_registry.async_get("sensor.wled_rgb_light_uptime") + assert entry + assert entry.unique_id == "aabbccddeeff_uptime" + + state = hass.states.get("sensor.wled_rgb_light_free_memory") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:memory" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_BYTES + assert state.state == "14600" + + entry = entity_registry.async_get("sensor.wled_rgb_light_free_memory") + assert entry + assert entry.unique_id == "aabbccddeeff_free_heap" From 856dcf0919d7a0e957b3d5ac42ce84607384c993 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Nov 2019 19:26:12 +0100 Subject: [PATCH 2/2] Process review comments --- homeassistant/components/wled/__init__.py | 10 +++++---- homeassistant/components/wled/const.py | 1 - homeassistant/components/wled/sensor.py | 27 +++++++++++++++-------- tests/components/wled/test_sensor.py | 15 +++++++++---- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 8566db49e58f47..cd2c091bc10e98 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -35,6 +35,7 @@ ) SCAN_INTERVAL = timedelta(seconds=5) +WLED_COMPONENTS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -61,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} # Set up all platforms for this device/entry. - for component in LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN: + for component in WLED_COMPONENTS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -94,9 +95,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Unload entities for this entry/device. await asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN), - hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), - hass.config_entries.async_forward_entry_unload(entry, SENSOR_DOMAIN), + *( + hass.config_entries.async_forward_entry_unload(entry, component) + for component in WLED_COMPONENTS + ) ) # Cleanup diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index ff2fd291262bd1..5fc24d74d63a25 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -31,4 +31,3 @@ # Units of measurement CURRENT_MA = "mA" DATA_BYTES = "bytes" -TIME_SECONDS = "seconds" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index e3faf2c108887a..f464b27e140fa9 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -1,20 +1,23 @@ """Support for WLED sensors.""" +from datetime import timedelta import logging -from typing import Callable, List, Union +from typing import Callable, List, Optional, Union -from homeassistant.components.wled import WLED, WLEDDeviceEntity -from homeassistant.components.wled.const import ( +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.dt import utcnow + +from . import WLED, WLEDDeviceEntity +from .const import ( ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DATA_BYTES, DATA_WLED_CLIENT, DOMAIN, - TIME_SECONDS, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -104,13 +107,19 @@ def __init__(self, entry_id: str, wled: WLED) -> None: wled, f"{wled.device.info.name} Uptime", "mdi:clock-outline", - TIME_SECONDS, + None, "uptime", ) + @property + def device_class(self) -> Optional[str]: + """Return the class of this sensor.""" + return DEVICE_CLASS_TIMESTAMP + async def _wled_update(self) -> None: """Update WLED uptime sensor.""" - self._state = self.wled.device.info.uptime + uptime = utcnow() - timedelta(seconds=self.wled.device.info.uptime) + self._state = uptime.replace(microsecond=0).isoformat() class WLEDFreeHeapSensor(WLEDSensor): diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index f1b092a783651c..a1247a8c373bce 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -1,13 +1,17 @@ """Tests for the WLED sensor platform.""" +from datetime import datetime + +from asynctest import patch + from homeassistant.components.wled.const import ( ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DATA_BYTES, - TIME_SECONDS, ) from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -17,7 +21,10 @@ async def test_sensors( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the creation and values of the WLED sensors.""" - await init_integration(hass, aioclient_mock) + + test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC) + with patch("homeassistant.components.wled.sensor.utcnow", return_value=test_time): + await init_integration(hass, aioclient_mock) entity_registry = await hass.helpers.entity_registry.async_get_registry() @@ -36,8 +43,8 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_uptime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_SECONDS - assert state.state == "32" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.state == "2019-11-11T09:10:00+00:00" entry = entity_registry.async_get("sensor.wled_rgb_light_uptime") assert entry