From 4b8217777e596b1b0b6112ce70e46bdaca0813f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Aug 2020 17:33:34 +0200 Subject: [PATCH] Add basic light and sensor support to Shelly (#39288) * Add basic light platform * Add sensor support * Bump aioshelly to 0.2.1 * Lint * Use UNIT_PERCENTAGE Co-authored-by: Maciej Bieniek * Format sensor.py Co-authored-by: Maciej Bieniek --- .coveragerc | 2 + homeassistant/components/shelly/__init__.py | 7 +- .../components/shelly/config_flow.py | 4 +- homeassistant/components/shelly/light.py | 74 +++++++++++++++++ homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/sensor.py | 83 +++++++++++++++++++ homeassistant/components/shelly/switch.py | 21 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 181 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/shelly/light.py create mode 100644 homeassistant/components/shelly/sensor.py diff --git a/.coveragerc b/.coveragerc index 72f5ffa5812..663031c1589 100644 --- a/.coveragerc +++ b/.coveragerc @@ -754,6 +754,8 @@ omit = homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py + homeassistant/components/shelly/light.py + homeassistant/components/shelly/sensor.py homeassistant/components/shelly/switch.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index f9b936898a1..093790644bb 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers import ( from .const import DOMAIN -PLATFORMS = ["switch"] +PLATFORMS = ["switch", "light", "sensor"] _LOGGER = logging.getLogger(__name__) @@ -129,11 +129,12 @@ class ShellyBlockEntity(entity.Entity): """Initialize Shelly entity.""" self.wrapper = wrapper self.block = block + self._name = f"{self.wrapper.name} - {self.block.description.replace('_', ' ')}" @property def name(self): """Name of entity.""" - return f"{self.wrapper.name} - {self.block.description}" + return self._name @property def should_poll(self): @@ -155,7 +156,7 @@ class ShellyBlockEntity(entity.Entity): @property def unique_id(self): """Return unique ID of entity.""" - return f"{self.wrapper.mac}-{self.block.index}" + return f"{self.wrapper.mac}-{self.block.description}" async def async_added_to_hass(self): """When entity is added to HASS.""" diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 830cb81c74e..c464f0d7adb 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -61,7 +61,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: device_info = await validate_input(self.hass, user_input) - except asyncio.TimeoutError: + except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") @@ -103,7 +103,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: device_info = await validate_input(self.hass, {"host": self.host}) - except asyncio.TimeoutError: + except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py new file mode 100644 index 00000000000..9e9b9e350a0 --- /dev/null +++ b/homeassistant/components/shelly/light.py @@ -0,0 +1,74 @@ +"""Light for Shelly.""" +from aioshelly import Block + +from homeassistant.components.light import SUPPORT_BRIGHTNESS, LightEntity +from homeassistant.core import callback + +from . import ShellyBlockEntity, ShellyDeviceWrapper +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up lights for device.""" + wrapper = hass.data[DOMAIN][config_entry.entry_id] + blocks = [block for block in wrapper.device.blocks if block.type == "light"] + + if not blocks: + return + + async_add_entities(ShellyLight(wrapper, block) for block in blocks) + + +class ShellyLight(ShellyBlockEntity, LightEntity): + """Switch that controls a relay block on Shelly devices.""" + + def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: + """Initialize light.""" + super().__init__(wrapper, block) + self.control_result = None + self._supported_features = 0 + if hasattr(block, "brightness"): + self._supported_features |= SUPPORT_BRIGHTNESS + + @property + def is_on(self) -> bool: + """If light is on.""" + if self.control_result: + return self.control_result["ison"] + + return self.block.output + + @property + def brightness(self): + """Brightness of light.""" + if self.control_result: + brightness = self.control_result["brightness"] + else: + brightness = self.block.brightness + return int(brightness / 100 * 255) + + @property + def supported_features(self): + """Supported features.""" + return self._supported_features + + async def async_turn_on( + self, brightness=None, **kwargs + ): # pylint: disable=arguments-differ + """Turn on light.""" + params = {"turn": "on"} + if brightness is not None: + params["brightness"] = int(brightness / 255 * 100) + self.control_result = await self.block.set_state(**params) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn off light.""" + self.control_result = await self.block.set_state(turn="off") + self.async_write_ha_state() + + @callback + def _update_callback(self): + """When device updates, clear control result that overrides state.""" + self.control_result = None + super()._update_callback() diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 149b8ef18cd..4f4e740a83f 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly2", - "requirements": ["aioshelly==0.1.2"], + "requirements": ["aioshelly==0.2.1"], "zeroconf": ["_http._tcp.local."], "codeowners": ["@balloob"] } diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py new file mode 100644 index 00000000000..2af6d1d91a9 --- /dev/null +++ b/homeassistant/components/shelly/sensor.py @@ -0,0 +1,83 @@ +"""Sensor for Shelly.""" +import aioshelly + +from homeassistant.components import sensor +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE +from homeassistant.helpers.entity import Entity + +from . import ShellyBlockEntity, ShellyDeviceWrapper +from .const import DOMAIN + +SENSORS = { + "extTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], + "humidity": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_HUMIDITY], +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors for device.""" + wrapper = hass.data[DOMAIN][config_entry.entry_id] + sensors = [] + + for block in wrapper.device.blocks: + if block.type != "sensor": + continue + + for attr in SENSORS: + if not hasattr(block, attr): + continue + + sensors.append(ShellySensor(wrapper, block, attr)) + + if sensors: + async_add_entities(sensors) + + +class ShellySensor(ShellyBlockEntity, Entity): + """Switch that controls a relay block on Shelly devices.""" + + def __init__( + self, + wrapper: ShellyDeviceWrapper, + block: aioshelly.Block, + attribute: str, + ) -> None: + """Initialize sensor.""" + super().__init__(wrapper, block) + self.attribute = attribute + unit, device_class = SENSORS[attribute] + info = block.info(attribute) + + if info[aioshelly.BLOCK_VALUE_TYPE] == aioshelly.BLOCK_VALUE_TYPE_TEMPERATURE: + if info[aioshelly.BLOCK_VALUE_UNIT] == "C": + unit = TEMP_CELSIUS + else: + unit = TEMP_FAHRENHEIT + + self._unit = unit + self._device_class = device_class + + @property + def unique_id(self): + """Return unique ID of entity.""" + return f"{super().unique_id}-{self.attribute}" + + @property + def name(self): + """Name of sensor.""" + return f"{self.wrapper.name} - {self.attribute}" + + @property + def state(self): + """Value of sensor.""" + return getattr(self.block, self.attribute) + + @property + def unit_of_measurement(self): + """Return unit of sensor.""" + return self._unit + + @property + def device_class(self): + """Device class of sensor.""" + return self._device_class diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 4a6c2a21b0b..9bd14c49ab3 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -1,22 +1,25 @@ """Switch for Shelly.""" -from homeassistant.components.shelly import ShellyBlockEntity +from aioshelly import RelayBlock + from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback +from . import ShellyBlockEntity, ShellyDeviceWrapper from .const import DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up switches for device.""" wrapper = hass.data[DOMAIN][config_entry.entry_id] + + if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": + return + relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"] if not relay_blocks: return - if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": - return - multiple_blocks = len(relay_blocks) > 1 async_add_entities( RelaySwitch(wrapper, block, multiple_blocks=multiple_blocks) @@ -27,9 +30,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RelaySwitch(ShellyBlockEntity, SwitchEntity): """Switch that controls a relay block on Shelly devices.""" - def __init__(self, *args, multiple_blocks) -> None: + def __init__( + self, wrapper: ShellyDeviceWrapper, block: RelayBlock, multiple_blocks + ) -> None: """Initialize relay switch.""" - super().__init__(*args) + super().__init__(wrapper, block) self.multiple_blocks = multiple_blocks self.control_result = None @@ -56,12 +61,12 @@ class RelaySwitch(ShellyBlockEntity, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn on relay.""" - self.control_result = await self.block.turn_on() + self.control_result = await self.block.set_state(turn="on") self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn off relay.""" - self.control_result = await self.block.turn_off() + self.control_result = await self.block.set_state(turn="off") self.async_write_ha_state() @callback diff --git a/requirements_all.txt b/requirements_all.txt index bb7f1aa0960..65d2e4cc161 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.1.2 +aioshelly==0.2.1 # homeassistant.components.switcher_kis aioswitcher==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c402aabd39e..2ea965c28b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.1.2 +aioshelly==0.2.1 # homeassistant.components.switcher_kis aioswitcher==1.2.0