From 982624b3ac59e9356f38e98eaa2657a05b5a364b Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 19 Nov 2020 12:42:24 +0200 Subject: [PATCH] Support for Shelly Binary Input Sensors (#43313) Co-authored-by: Maciej Bieniek Co-authored-by: Paulus Schoutsen --- .../components/shelly/binary_sensor.py | 20 +++++++++ homeassistant/components/shelly/entity.py | 16 ++++++- homeassistant/components/shelly/light.py | 6 +-- homeassistant/components/shelly/sensor.py | 44 ++++--------------- homeassistant/components/shelly/switch.py | 5 +-- homeassistant/components/shelly/utils.py | 37 +++++++++++----- 6 files changed, 73 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 00f63ea7411..62cd9aea8ce 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -4,6 +4,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_OPENING, + DEVICE_CLASS_POWER, DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, @@ -18,6 +19,7 @@ from .entity import ( async_setup_entry_attribute_entities, async_setup_entry_rest, ) +from .utils import is_momentary_input SENSORS = { ("device", "overtemp"): BlockAttributeDescription( @@ -50,6 +52,24 @@ SENSORS = { ("sensor", "vibration"): BlockAttributeDescription( name="Vibration", device_class=DEVICE_CLASS_VIBRATION ), + ("input", "input"): BlockAttributeDescription( + name="Input", + device_class=DEVICE_CLASS_POWER, + default_enabled=False, + removal_condition=is_momentary_input, + ), + ("relay", "input"): BlockAttributeDescription( + name="Input", + device_class=DEVICE_CLASS_POWER, + default_enabled=False, + removal_condition=is_momentary_input, + ), + ("device", "input"): BlockAttributeDescription( + name="Input", + device_class=DEVICE_CLASS_POWER, + default_enabled=False, + removal_condition=is_momentary_input, + ), } REST_SENSORS = { diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 047c3d9d66a..9b834db923c 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -9,7 +9,7 @@ from homeassistant.helpers import device_registry, entity, update_coordinator from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST -from .utils import get_entity_name, get_rest_value_from_path +from .utils import async_remove_shelly_entity, get_entity_name, get_rest_value_from_path async def async_setup_entry_attribute_entities( @@ -31,7 +31,17 @@ async def async_setup_entry_attribute_entities( if getattr(block, sensor_id, None) in (-1, None): continue - blocks.append((block, sensor_id, description)) + # Filter and remove entities that according to settings should not create an entity + if description.removal_condition and description.removal_condition( + wrapper.device.settings, block + ): + domain = sensor_class.__module__.split(".")[-1] + unique_id = sensor_class( + wrapper, block, sensor_id, description + ).unique_id + await async_remove_shelly_entity(hass, domain, unique_id) + else: + blocks.append((block, sensor_id, description)) if not blocks: return @@ -77,6 +87,8 @@ class BlockAttributeDescription: device_class: Optional[str] = None default_enabled: bool = True available: Optional[Callable[[aioshelly.Block], bool]] = None + # Callable (settings, block), return true if entity should be removed + removal_condition: Optional[Callable[[dict, aioshelly.Block], bool]] = None device_state_attributes: Optional[ Callable[[aioshelly.Block], Optional[dict]] ] = None diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 83c7f3cf177..b3a6869d67d 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -19,7 +19,7 @@ from homeassistant.util.color import ( from . import ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .entity import ShellyBlockEntity -from .utils import async_remove_entity_by_domain +from .utils import async_remove_shelly_entity async def async_setup_entry(hass, config_entry, async_add_entities): @@ -39,9 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): unique_id = ( f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}' ) - await async_remove_entity_by_domain( - hass, "switch", unique_id, config_entry.entry_id - ) + await async_remove_shelly_entity(hass, "switch", unique_id) if not blocks: return diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 81dc2ef1c11..10d15fdd62f 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,6 +1,4 @@ """Sensor for Shelly.""" -import logging - from homeassistant.components import sensor from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, @@ -14,8 +12,7 @@ from homeassistant.const import ( VOLT, ) -from . import ShellyDeviceWrapper, get_device_name -from .const import DATA_CONFIG_ENTRY, DOMAIN, REST, SHAIR_MAX_WORK_HOURS +from .const import SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, RestAttributeDescription, @@ -24,17 +21,15 @@ from .entity import ( async_setup_entry_attribute_entities, async_setup_entry_rest, ) -from .utils import async_remove_entity_by_domain, temperature_unit - -_LOGGER = logging.getLogger(__name__) - -BATTERY_SENSOR = { - ("device", "battery"): BlockAttributeDescription( - name="Battery", unit=PERCENTAGE, device_class=sensor.DEVICE_CLASS_BATTERY - ), -} +from .utils import temperature_unit SENSORS = { + ("device", "battery"): BlockAttributeDescription( + name="Battery", + unit=PERCENTAGE, + device_class=sensor.DEVICE_CLASS_BATTERY, + removal_condition=lambda settings, _: settings.get("external_power") == 1, + ), ("device", "deviceTemp"): BlockAttributeDescription( name="Device Temperature", unit=temperature_unit, @@ -176,6 +171,7 @@ REST_SENSORS = { "uptime": RestAttributeDescription( name="Uptime", device_class=sensor.DEVICE_CLASS_TIMESTAMP, + default_enabled=False, path="uptime", ), } @@ -183,28 +179,6 @@ REST_SENSORS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for device.""" - - wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ][REST] - - if ( - "external_power" in wrapper.device.settings - and wrapper.device.settings["external_power"] == 1 - ): - _LOGGER.debug( - "Removed battery sensor [externally powered] for %s", - get_device_name(wrapper.device), - ) - unique_id = f'{wrapper.device.shelly["mac"]}-battery' - await async_remove_entity_by_domain( - hass, "sensor", unique_id, config_entry.entry_id - ) - else: - await async_setup_entry_attribute_entities( - hass, config_entry, async_add_entities, BATTERY_SENSOR, ShellySensor - ) - await async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, SENSORS, ShellySensor ) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 653f090bf4e..c86487072c6 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from . import ShellyDeviceWrapper from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .entity import ShellyBlockEntity -from .utils import async_remove_entity_by_domain +from .utils import async_remove_shelly_entity async def async_setup_entry(hass, config_entry, async_add_entities): @@ -32,11 +32,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): unique_id = ( f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}' ) - await async_remove_entity_by_domain( + await async_remove_shelly_entity( hass, "light", unique_id, - config_entry.entry_id, ) if not relay_blocks: diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index c9ef3f55adb..e6981db2a0d 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -8,24 +8,20 @@ import aioshelly from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers import entity_registry from . import ShellyDeviceWrapper +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_remove_entity_by_domain(hass, domain, unique_id, config_entry_id): - """Remove entity by domain.""" - +async def async_remove_shelly_entity(hass, domain, unique_id): + """Remove a Shelly entity.""" entity_reg = await hass.helpers.entity_registry.async_get_registry() - for entry in entity_registry.async_entries_for_config_entry( - entity_reg, config_entry_id - ): - if entry.domain == domain and entry.unique_id == unique_id: - entity_reg.async_remove(entry.entity_id) - _LOGGER.debug("Removed %s domain for %s", domain, entry.original_name) - break + entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id) + if entity_id: + _LOGGER.debug("Removing entity: %s", entity_id) + entity_reg.async_remove(entity_id) def temperature_unit(block_info: dict) -> str: @@ -92,4 +88,23 @@ def get_rest_value_from_path(status, device_class, path: str): last_boot = datetime.utcnow() - timedelta(seconds=attribute_value) attribute_value = last_boot.replace(microsecond=0).isoformat() + if "new_version" in path: + attribute_value = attribute_value.split("/")[1].split("@")[0] + return attribute_value + + +def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: + """Return true if input button settings is set to a momentary type.""" + button = settings.get("relays") or settings.get("lights") or settings.get("inputs") + + # Shelly 1L has two button settings in the first channel + if settings["device"]["type"] == "SHSW-L": + channel = int(block.channel or 0) + 1 + button_type = button[0].get("btn" + str(channel) + "_type") + else: + # Some devices has only one channel in settings + channel = min(int(block.channel or 0), len(button) - 1) + button_type = button[channel].get("btn_type") + + return button_type in ["momentary", "momentary_on_release"]