hass-core/homeassistant/components/shelly/utils.py
Shay Levy 76537305e2
Add logbook and device trigger platforms to Shelly (#44020)
* Add logbook and device trigger platforms to Shelly

Add `logbook` platform for describing “shelly.click” event
Add `device_trigger` platform for adding automation based on click events:

Example of logbook event:
Shelly 'single' click event for Test I3 channel 3 was fired.
(Test I3 is the name of the device)

Example of automation triggers:
First button triple clicked
First button long clicked and then single clicked
First button double clicked
First button long clicked
First button single clicked and then long clicked
First button single clicked
Second button triple clicked
..
Second button single clicked

* Fix codespell

* Remove pylint added for debug

* Add tests

* Rebase

* Fix Rebase & Apply PR review suggestions

Fix tests after rebasing
Use `INPUTS_EVENTS_DICT` for input triggers
Apply PR suggestions
2021-01-04 23:10:42 +01:00

181 lines
5.4 KiB
Python

"""Shelly helpers functions."""
from datetime import timedelta
import logging
from typing import List, Optional, Tuple
import aioshelly
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import parse_datetime, utcnow
from .const import (
BASIC_INPUTS_EVENTS_TYPES,
COAP,
DATA_CONFIG_ENTRY,
DOMAIN,
SHBTN_1_INPUTS_EVENTS_TYPES,
SHIX3_1_INPUTS_EVENTS_TYPES,
)
_LOGGER = logging.getLogger(__name__)
async def async_remove_shelly_entity(hass, domain, unique_id):
"""Remove a Shelly entity."""
entity_reg = await hass.helpers.entity_registry.async_get_registry()
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:
"""Detect temperature unit."""
if block_info[aioshelly.BLOCK_VALUE_UNIT] == "F":
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
def get_device_name(device: aioshelly.Device) -> str:
"""Naming for device."""
return device.settings["name"] or device.settings["device"]["hostname"]
def get_number_of_channels(device: aioshelly.Device, block: aioshelly.Block) -> int:
"""Get number of channels for block type."""
channels = None
if block.type == "input":
# Shelly Dimmer/1L has two input channels and missing "num_inputs"
if device.settings["device"]["type"] in ["SHDM-1", "SHDM-2", "SHSW-L"]:
channels = 2
else:
channels = device.shelly.get("num_inputs")
elif block.type == "emeter":
channels = device.shelly.get("num_emeters")
elif block.type in ["relay", "light"]:
channels = device.shelly.get("num_outputs")
elif block.type in ["roller", "device"]:
channels = 1
return channels or 1
def get_entity_name(
device: aioshelly.Device,
block: aioshelly.Block,
description: Optional[str] = None,
) -> str:
"""Naming for switch and sensors."""
channel_name = get_device_channel_name(device, block)
if description:
return f"{channel_name} {description}"
return channel_name
def get_device_channel_name(
device: aioshelly.Device,
block: aioshelly.Block,
) -> str:
"""Get name based on device and channel name."""
entity_name = get_device_name(device)
if (
not block
or block.type == "device"
or get_number_of_channels(device, block) == 1
):
return entity_name
channel_name = None
mode = block.type + "s"
if mode in device.settings:
channel_name = device.settings[mode][int(block.channel)].get("name")
if channel_name:
return channel_name
if device.settings["device"]["type"] == "SHEM-3":
base = ord("A")
else:
base = ord("1")
return f"{entity_name} channel {chr(int(block.channel)+base)}"
def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool:
"""Return true if input button settings is set to a momentary type."""
# Shelly Button type is fixed to momentary and no btn_type
if settings["device"]["type"] == "SHBTN-1":
return True
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"]
def get_device_uptime(status: dict, last_uptime: str) -> str:
"""Return device uptime string, tolerate up to 5 seconds deviation."""
uptime = utcnow() - timedelta(seconds=status["uptime"])
if not last_uptime:
return uptime.replace(microsecond=0).isoformat()
if abs((uptime - parse_datetime(last_uptime)).total_seconds()) > 5:
return uptime.replace(microsecond=0).isoformat()
return last_uptime
def get_input_triggers(
device: aioshelly.Device, block: aioshelly.Block
) -> List[Tuple[str, str]]:
"""Return list of input triggers for block."""
if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids:
return []
if not is_momentary_input(device.settings, block):
return []
triggers = []
if block.type == "device" or get_number_of_channels(device, block) == 1:
subtype = "button"
else:
subtype = f"button{int(block.channel)+1}"
if device.settings["device"]["type"] == "SHBTN-1":
trigger_types = SHBTN_1_INPUTS_EVENTS_TYPES
elif device.settings["device"]["type"] == "SHIX3-1":
trigger_types = SHIX3_1_INPUTS_EVENTS_TYPES
else:
trigger_types = BASIC_INPUTS_EVENTS_TYPES
for trigger_type in trigger_types:
triggers.append((trigger_type, subtype))
return triggers
def get_device_wrapper(hass: HomeAssistant, device_id: str):
"""Get a Shelly device wrapper for the given device id."""
for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]:
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry][COAP]
if wrapper.device_id == device_id:
return wrapper
return None