Add support for Shelly battery operated devices (#45406)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>pull/45930/head
parent
fcc14933d0
commit
0875f654c8
|
@ -17,12 +17,7 @@ from homeassistant.const import (
|
|||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
device_registry,
|
||||
singleton,
|
||||
update_coordinator,
|
||||
)
|
||||
from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator
|
||||
|
||||
from .const import (
|
||||
AIOSHELLY_DEVICE_TIMEOUT_SEC,
|
||||
|
@ -32,36 +27,23 @@ from .const import (
|
|||
BATTERY_DEVICES_WITH_PERMANENT_CONNECTION,
|
||||
COAP,
|
||||
DATA_CONFIG_ENTRY,
|
||||
DEVICE,
|
||||
DOMAIN,
|
||||
EVENT_SHELLY_CLICK,
|
||||
INPUTS_EVENTS_DICT,
|
||||
POLLING_TIMEOUT_MULTIPLIER,
|
||||
POLLING_TIMEOUT_SEC,
|
||||
REST,
|
||||
REST_SENSORS_UPDATE_INTERVAL,
|
||||
SLEEP_PERIOD_MULTIPLIER,
|
||||
UPDATE_PERIOD_MULTIPLIER,
|
||||
)
|
||||
from .utils import get_device_name
|
||||
from .utils import get_coap_context, get_device_name, get_device_sleep_period
|
||||
|
||||
PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"]
|
||||
SLEEPING_PLATFORMS = ["binary_sensor", "sensor"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@singleton.singleton("shelly_coap")
|
||||
async def get_coap_context(hass):
|
||||
"""Get CoAP context to be used in all Shelly devices."""
|
||||
context = aioshelly.COAP()
|
||||
await context.initialize()
|
||||
|
||||
@callback
|
||||
def shutdown_listener(ev):
|
||||
context.close()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the Shelly component."""
|
||||
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
|
||||
|
@ -70,6 +52,9 @@ async def async_setup(hass: HomeAssistant, config: dict):
|
|||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Shelly from a config entry."""
|
||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {}
|
||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None
|
||||
|
||||
temperature_unit = "C" if hass.config.units.is_metric else "F"
|
||||
|
||||
ip_address = await hass.async_add_executor_job(gethostbyname, entry.data[CONF_HOST])
|
||||
|
@ -83,33 +68,79 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
|
||||
coap_context = await get_coap_context(hass)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
|
||||
device = await aioshelly.Device.create(
|
||||
aiohttp_client.async_get_clientsession(hass),
|
||||
coap_context,
|
||||
options,
|
||||
)
|
||||
except (asyncio.TimeoutError, OSError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
device = await aioshelly.Device.create(
|
||||
aiohttp_client.async_get_clientsession(hass),
|
||||
coap_context,
|
||||
options,
|
||||
False,
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {}
|
||||
coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
|
||||
dev_reg = await device_registry.async_get_registry(hass)
|
||||
identifier = (DOMAIN, entry.unique_id)
|
||||
device_entry = dev_reg.async_get_device(identifiers={identifier}, connections=set())
|
||||
|
||||
sleep_period = entry.data.get("sleep_period")
|
||||
|
||||
@callback
|
||||
def _async_device_online(_):
|
||||
_LOGGER.debug("Device %s is online, resuming setup", entry.title)
|
||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None
|
||||
|
||||
if sleep_period is None:
|
||||
data = {**entry.data}
|
||||
data["sleep_period"] = get_device_sleep_period(device.settings)
|
||||
data["model"] = device.settings["device"]["type"]
|
||||
hass.config_entries.async_update_entry(entry, data=data)
|
||||
|
||||
hass.async_create_task(async_device_setup(hass, entry, device))
|
||||
|
||||
if sleep_period == 0:
|
||||
# Not a sleeping device, finish setup
|
||||
_LOGGER.debug("Setting up online device %s", entry.title)
|
||||
try:
|
||||
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
|
||||
await device.initialize(True)
|
||||
except (asyncio.TimeoutError, OSError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
await async_device_setup(hass, entry, device)
|
||||
elif sleep_period is None or device_entry is None:
|
||||
# Need to get sleep info or first time sleeping device setup, wait for device
|
||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = device
|
||||
_LOGGER.debug(
|
||||
"Setup for device %s will resume when device is online", entry.title
|
||||
)
|
||||
device.subscribe_updates(_async_device_online)
|
||||
else:
|
||||
# Restore sensors for sleeping device
|
||||
_LOGGER.debug("Setting up offline device %s", entry.title)
|
||||
await async_device_setup(hass, entry, device)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_device_setup(
|
||||
hass: HomeAssistant, entry: ConfigEntry, device: aioshelly.Device
|
||||
):
|
||||
"""Set up a device that is online."""
|
||||
device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
|
||||
COAP
|
||||
] = ShellyDeviceWrapper(hass, entry, device)
|
||||
await coap_wrapper.async_setup()
|
||||
await device_wrapper.async_setup()
|
||||
|
||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
|
||||
REST
|
||||
] = ShellyDeviceRestWrapper(hass, device)
|
||||
platforms = SLEEPING_PLATFORMS
|
||||
|
||||
for component in PLATFORMS:
|
||||
if not entry.data.get("sleep_period"):
|
||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
|
||||
REST
|
||||
] = ShellyDeviceRestWrapper(hass, device)
|
||||
platforms = PLATFORMS
|
||||
|
||||
for component in platforms:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
||||
"""Wrapper for a Shelly device with Home Assistant specific functions."""
|
||||
|
@ -117,43 +148,40 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||
def __init__(self, hass, entry, device: aioshelly.Device):
|
||||
"""Initialize the Shelly device wrapper."""
|
||||
self.device_id = None
|
||||
sleep_mode = device.settings.get("sleep_mode")
|
||||
sleep_period = entry.data["sleep_period"]
|
||||
|
||||
if sleep_mode:
|
||||
sleep_period = sleep_mode["period"]
|
||||
if sleep_mode["unit"] == "h":
|
||||
sleep_period *= 60 # hours to minutes
|
||||
|
||||
update_interval = (
|
||||
SLEEP_PERIOD_MULTIPLIER * sleep_period * 60
|
||||
) # minutes to seconds
|
||||
if sleep_period:
|
||||
update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period
|
||||
else:
|
||||
update_interval = (
|
||||
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
|
||||
)
|
||||
|
||||
device_name = get_device_name(device) if device.initialized else entry.title
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=get_device_name(device),
|
||||
name=device_name,
|
||||
update_interval=timedelta(seconds=update_interval),
|
||||
)
|
||||
self.hass = hass
|
||||
self.entry = entry
|
||||
self.device = device
|
||||
|
||||
self.device.subscribe_updates(self.async_set_updated_data)
|
||||
|
||||
self._async_remove_input_events_handler = self.async_add_listener(
|
||||
self._async_input_events_handler
|
||||
self._async_remove_device_updates_handler = self.async_add_listener(
|
||||
self._async_device_updates_handler
|
||||
)
|
||||
self._last_input_events_count = dict()
|
||||
self._last_input_events_count = {}
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
|
||||
|
||||
@callback
|
||||
def _async_input_events_handler(self):
|
||||
"""Handle device input events."""
|
||||
def _async_device_updates_handler(self):
|
||||
"""Handle device updates."""
|
||||
if not self.device.initialized:
|
||||
return
|
||||
|
||||
# Check for input events
|
||||
for block in self.device.blocks:
|
||||
if (
|
||||
"inputEvent" not in block.sensor_ids
|
||||
|
@ -192,13 +220,9 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||
|
||||
async def _async_update_data(self):
|
||||
"""Fetch data."""
|
||||
|
||||
_LOGGER.debug("Polling Shelly Device - %s", self.name)
|
||||
try:
|
||||
async with async_timeout.timeout(
|
||||
POLLING_TIMEOUT_MULTIPLIER
|
||||
* self.device.settings["coiot"]["update_period"]
|
||||
):
|
||||
async with async_timeout.timeout(POLLING_TIMEOUT_SEC):
|
||||
return await self.device.update()
|
||||
except OSError as err:
|
||||
raise update_coordinator.UpdateFailed("Error fetching data") from err
|
||||
|
@ -206,18 +230,17 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||
@property
|
||||
def model(self):
|
||||
"""Model of the device."""
|
||||
return self.device.settings["device"]["type"]
|
||||
return self.entry.data["model"]
|
||||
|
||||
@property
|
||||
def mac(self):
|
||||
"""Mac address of the device."""
|
||||
return self.device.settings["device"]["mac"]
|
||||
return self.entry.unique_id
|
||||
|
||||
async def async_setup(self):
|
||||
"""Set up the wrapper."""
|
||||
|
||||
dev_reg = await device_registry.async_get_registry(self.hass)
|
||||
model_type = self.device.settings["device"]["type"]
|
||||
sw_version = self.device.settings["fw"] if self.device.initialized else ""
|
||||
entry = dev_reg.async_get_or_create(
|
||||
config_entry_id=self.entry.entry_id,
|
||||
name=self.name,
|
||||
|
@ -225,15 +248,16 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||
# This is duplicate but otherwise via_device can't work
|
||||
identifiers={(DOMAIN, self.mac)},
|
||||
manufacturer="Shelly",
|
||||
model=aioshelly.MODEL_NAMES.get(model_type, model_type),
|
||||
sw_version=self.device.settings["fw"],
|
||||
model=aioshelly.MODEL_NAMES.get(self.model, self.model),
|
||||
sw_version=sw_version,
|
||||
)
|
||||
self.device_id = entry.id
|
||||
self.device.subscribe_updates(self.async_set_updated_data)
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown the wrapper."""
|
||||
self.device.shutdown()
|
||||
self._async_remove_input_events_handler()
|
||||
self._async_remove_device_updates_handler()
|
||||
|
||||
@callback
|
||||
def _handle_ha_stop(self, _):
|
||||
|
@ -282,11 +306,23 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator):
|
|||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE)
|
||||
if device is not None:
|
||||
# If device is present, device wrapper is not setup yet
|
||||
device.shutdown()
|
||||
return True
|
||||
|
||||
platforms = SLEEPING_PLATFORMS
|
||||
|
||||
if not entry.data.get("sleep_period"):
|
||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][REST] = None
|
||||
platforms = PLATFORMS
|
||||
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
for component in platforms
|
||||
]
|
||||
)
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import (
|
|||
DEVICE_CLASS_PROBLEM,
|
||||
DEVICE_CLASS_SMOKE,
|
||||
DEVICE_CLASS_VIBRATION,
|
||||
STATE_ON,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
|
||||
|
@ -17,6 +18,7 @@ from .entity import (
|
|||
RestAttributeDescription,
|
||||
ShellyBlockAttributeEntity,
|
||||
ShellyRestAttributeEntity,
|
||||
ShellySleepingBlockAttributeEntity,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rest,
|
||||
)
|
||||
|
@ -98,13 +100,25 @@ REST_SENSORS = {
|
|||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up sensors for device."""
|
||||
await async_setup_entry_attribute_entities(
|
||||
hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor
|
||||
)
|
||||
|
||||
await async_setup_entry_rest(
|
||||
hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestBinarySensor
|
||||
)
|
||||
if config_entry.data["sleep_period"]:
|
||||
await async_setup_entry_attribute_entities(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
SENSORS,
|
||||
ShellySleepingBinarySensor,
|
||||
)
|
||||
else:
|
||||
await async_setup_entry_attribute_entities(
|
||||
hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor
|
||||
)
|
||||
await async_setup_entry_rest(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
REST_SENSORS,
|
||||
ShellyRestBinarySensor,
|
||||
)
|
||||
|
||||
|
||||
class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity):
|
||||
|
@ -123,3 +137,17 @@ class ShellyRestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity):
|
|||
def is_on(self):
|
||||
"""Return true if REST sensor state is on."""
|
||||
return bool(self.attribute_value)
|
||||
|
||||
|
||||
class ShellySleepingBinarySensor(
|
||||
ShellySleepingBlockAttributeEntity, BinarySensorEntity
|
||||
):
|
||||
"""Represent a shelly sleeping binary sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor state is on."""
|
||||
if self.block is not None:
|
||||
return bool(self.attribute_value)
|
||||
|
||||
return self.last_state == STATE_ON
|
||||
|
|
|
@ -17,9 +17,9 @@ from homeassistant.const import (
|
|||
)
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from . import get_coap_context
|
||||
from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
from .utils import get_coap_context, get_device_sleep_period
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -53,6 +53,8 @@ async def validate_input(hass: core.HomeAssistant, host, data):
|
|||
return {
|
||||
"title": device.settings["name"],
|
||||
"hostname": device.settings["device"]["hostname"],
|
||||
"sleep_period": get_device_sleep_period(device.settings),
|
||||
"model": device.settings["device"]["type"],
|
||||
}
|
||||
|
||||
|
||||
|
@ -95,7 +97,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
else:
|
||||
return self.async_create_entry(
|
||||
title=device_info["title"] or device_info["hostname"],
|
||||
data=user_input,
|
||||
data={
|
||||
**user_input,
|
||||
"sleep_period": device_info["sleep_period"],
|
||||
"model": device_info["model"],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
@ -121,7 +127,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
else:
|
||||
return self.async_create_entry(
|
||||
title=device_info["title"] or device_info["hostname"],
|
||||
data={**user_input, CONF_HOST: self.host},
|
||||
data={
|
||||
**user_input,
|
||||
CONF_HOST: self.host,
|
||||
"sleep_period": device_info["sleep_period"],
|
||||
"model": device_info["model"],
|
||||
},
|
||||
)
|
||||
else:
|
||||
user_input = {}
|
||||
|
@ -172,7 +183,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
else:
|
||||
return self.async_create_entry(
|
||||
title=device_info["title"] or device_info["hostname"],
|
||||
data={"host": self.host},
|
||||
data={
|
||||
"host": self.host,
|
||||
"sleep_period": device_info["sleep_period"],
|
||||
"model": device_info["model"],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
COAP = "coap"
|
||||
DATA_CONFIG_ENTRY = "config_entry"
|
||||
DEVICE = "device"
|
||||
DOMAIN = "shelly"
|
||||
REST = "rest"
|
||||
|
||||
# Used to calculate the timeout in "_async_update_data" used for polling data from devices.
|
||||
POLLING_TIMEOUT_MULTIPLIER = 1.2
|
||||
# Used in "_async_update_data" as timeout for polling data from devices.
|
||||
POLLING_TIMEOUT_SEC = 18
|
||||
|
||||
# Refresh interval for REST sensors
|
||||
REST_SENSORS_UPDATE_INTERVAL = 60
|
||||
|
|
|
@ -1,24 +1,49 @@
|
|||
"""Shelly entity helper."""
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
import aioshelly
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry, entity, update_coordinator
|
||||
from homeassistant.helpers import (
|
||||
device_registry,
|
||||
entity,
|
||||
entity_registry,
|
||||
update_coordinator,
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper
|
||||
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST
|
||||
from .utils import async_remove_shelly_entity, get_entity_name
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry_attribute_entities(
|
||||
hass, config_entry, async_add_entities, sensors, sensor_class
|
||||
):
|
||||
"""Set up entities for block attributes."""
|
||||
"""Set up entities for attributes."""
|
||||
wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
|
||||
config_entry.entry_id
|
||||
][COAP]
|
||||
|
||||
if wrapper.device.initialized:
|
||||
await async_setup_block_attribute_entities(
|
||||
hass, async_add_entities, wrapper, sensors, sensor_class
|
||||
)
|
||||
else:
|
||||
await async_restore_block_attribute_entities(
|
||||
hass, config_entry, async_add_entities, wrapper, sensor_class
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_block_attribute_entities(
|
||||
hass, async_add_entities, wrapper, sensors, sensor_class
|
||||
):
|
||||
"""Set up entities for block attributes."""
|
||||
blocks = []
|
||||
|
||||
for block in wrapper.device.blocks:
|
||||
|
@ -36,9 +61,7 @@ async def async_setup_entry_attribute_entities(
|
|||
wrapper.device.settings, block
|
||||
):
|
||||
domain = sensor_class.__module__.split(".")[-1]
|
||||
unique_id = sensor_class(
|
||||
wrapper, block, sensor_id, description
|
||||
).unique_id
|
||||
unique_id = f"{wrapper.mac}-{block.description}-{sensor_id}"
|
||||
await async_remove_shelly_entity(hass, domain, unique_id)
|
||||
else:
|
||||
blocks.append((block, sensor_id, description))
|
||||
|
@ -54,6 +77,39 @@ async def async_setup_entry_attribute_entities(
|
|||
)
|
||||
|
||||
|
||||
async def async_restore_block_attribute_entities(
|
||||
hass, config_entry, async_add_entities, wrapper, sensor_class
|
||||
):
|
||||
"""Restore block attributes entities."""
|
||||
entities = []
|
||||
|
||||
ent_reg = await entity_registry.async_get_registry(hass)
|
||||
entries = entity_registry.async_entries_for_config_entry(
|
||||
ent_reg, config_entry.entry_id
|
||||
)
|
||||
|
||||
domain = sensor_class.__module__.split(".")[-1]
|
||||
|
||||
for entry in entries:
|
||||
if entry.domain != domain:
|
||||
continue
|
||||
|
||||
attribute = entry.unique_id.split("-")[-1]
|
||||
description = BlockAttributeDescription(
|
||||
name="",
|
||||
icon=entry.original_icon,
|
||||
unit=entry.unit_of_measurement,
|
||||
device_class=entry.device_class,
|
||||
)
|
||||
|
||||
entities.append(sensor_class(wrapper, None, attribute, description, entry))
|
||||
|
||||
if not entities:
|
||||
return
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
async def async_setup_entry_rest(
|
||||
hass, config_entry, async_add_entities, sensors, sensor_class
|
||||
):
|
||||
|
@ -163,7 +219,7 @@ class ShellyBlockEntity(entity.Entity):
|
|||
|
||||
|
||||
class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
|
||||
"""Switch that controls a relay block on Shelly devices."""
|
||||
"""Helper class to represent a block attribute."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -176,12 +232,11 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
|
|||
super().__init__(wrapper, block)
|
||||
self.attribute = attribute
|
||||
self.description = description
|
||||
self.info = block.info(attribute)
|
||||
|
||||
unit = self.description.unit
|
||||
|
||||
if callable(unit):
|
||||
unit = unit(self.info)
|
||||
unit = unit(block.info(attribute))
|
||||
|
||||
self._unit = unit
|
||||
self._unique_id = f"{super().unique_id}-{self.attribute}"
|
||||
|
@ -320,3 +375,67 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
|
|||
return None
|
||||
|
||||
return self.description.device_state_attributes(self.wrapper.device.status)
|
||||
|
||||
|
||||
class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEntity):
|
||||
"""Represent a shelly sleeping block attribute entity."""
|
||||
|
||||
# pylint: disable=super-init-not-called
|
||||
def __init__(
|
||||
self,
|
||||
wrapper: ShellyDeviceWrapper,
|
||||
block: aioshelly.Block,
|
||||
attribute: str,
|
||||
description: BlockAttributeDescription,
|
||||
entry: Optional[ConfigEntry] = None,
|
||||
) -> None:
|
||||
"""Initialize the sleeping sensor."""
|
||||
self.last_state = None
|
||||
self.wrapper = wrapper
|
||||
self.attribute = attribute
|
||||
self.block = block
|
||||
self.description = description
|
||||
self._unit = self.description.unit
|
||||
|
||||
if block is not None:
|
||||
if callable(self._unit):
|
||||
self._unit = self._unit(block.info(attribute))
|
||||
|
||||
self._unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}"
|
||||
self._name = get_entity_name(
|
||||
self.wrapper.device, block, self.description.name
|
||||
)
|
||||
else:
|
||||
self._unique_id = entry.unique_id
|
||||
self._name = entry.original_name
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
last_state = await self.async_get_last_state()
|
||||
|
||||
if last_state is not None:
|
||||
self.last_state = last_state.state
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Handle device update."""
|
||||
if self.block is not None:
|
||||
super()._update_callback()
|
||||
return
|
||||
|
||||
_, entity_block, entity_sensor = self.unique_id.split("-")
|
||||
|
||||
for block in self.wrapper.device.blocks:
|
||||
if block.description != entity_block:
|
||||
continue
|
||||
|
||||
for sensor_id in block.sensor_ids:
|
||||
if sensor_id != entity_sensor:
|
||||
continue
|
||||
|
||||
self.block = block
|
||||
_LOGGER.debug("Entity %s attached to block", self.name)
|
||||
super()._update_callback()
|
||||
return
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Shelly",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||
"requirements": ["aioshelly==0.5.3"],
|
||||
"requirements": ["aioshelly==0.5.4"],
|
||||
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
|
||||
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"]
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ from .entity import (
|
|||
RestAttributeDescription,
|
||||
ShellyBlockAttributeEntity,
|
||||
ShellyRestAttributeEntity,
|
||||
ShellySleepingBlockAttributeEntity,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rest,
|
||||
)
|
||||
|
@ -185,12 +186,17 @@ REST_SENSORS = {
|
|||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up sensors for device."""
|
||||
await async_setup_entry_attribute_entities(
|
||||
hass, config_entry, async_add_entities, SENSORS, ShellySensor
|
||||
)
|
||||
await async_setup_entry_rest(
|
||||
hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestSensor
|
||||
)
|
||||
if config_entry.data["sleep_period"]:
|
||||
await async_setup_entry_attribute_entities(
|
||||
hass, config_entry, async_add_entities, SENSORS, ShellySleepingSensor
|
||||
)
|
||||
else:
|
||||
await async_setup_entry_attribute_entities(
|
||||
hass, config_entry, async_add_entities, SENSORS, ShellySensor
|
||||
)
|
||||
await async_setup_entry_rest(
|
||||
hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestSensor
|
||||
)
|
||||
|
||||
|
||||
class ShellySensor(ShellyBlockAttributeEntity):
|
||||
|
@ -209,3 +215,15 @@ class ShellyRestSensor(ShellyRestAttributeEntity):
|
|||
def state(self):
|
||||
"""Return value of sensor."""
|
||||
return self.attribute_value
|
||||
|
||||
|
||||
class ShellySleepingSensor(ShellySleepingBlockAttributeEntity):
|
||||
"""Represent a shelly sleeping sensor."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return value of sensor."""
|
||||
if self.block is not None:
|
||||
return self.attribute_value
|
||||
|
||||
return self.last_state
|
||||
|
|
|
@ -6,8 +6,9 @@ from typing import List, Optional, Tuple
|
|||
|
||||
import aioshelly
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import singleton
|
||||
from homeassistant.util.dt import parse_datetime, utcnow
|
||||
|
||||
from .const import (
|
||||
|
@ -182,3 +183,30 @@ def get_device_wrapper(hass: HomeAssistant, device_id: str):
|
|||
return wrapper
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@singleton.singleton("shelly_coap")
|
||||
async def get_coap_context(hass):
|
||||
"""Get CoAP context to be used in all Shelly devices."""
|
||||
context = aioshelly.COAP()
|
||||
await context.initialize()
|
||||
|
||||
@callback
|
||||
def shutdown_listener(ev):
|
||||
context.close()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def get_device_sleep_period(settings: dict) -> int:
|
||||
"""Return the device sleep period in seconds or 0 for non sleeping devices."""
|
||||
sleep_period = 0
|
||||
|
||||
if settings.get("sleep_mode", False):
|
||||
sleep_period = settings["sleep_mode"]["period"]
|
||||
if settings["sleep_mode"]["unit"] == "h":
|
||||
sleep_period *= 60 # hours to minutes
|
||||
|
||||
return sleep_period * 60 # minutes to seconds
|
||||
|
|
|
@ -218,7 +218,7 @@ aiopylgtv==0.3.3
|
|||
aiorecollect==1.0.1
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==0.5.3
|
||||
aioshelly==0.5.4
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==1.2.1
|
||||
|
|
|
@ -137,7 +137,7 @@ aiopylgtv==0.3.3
|
|||
aiorecollect==1.0.1
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==0.5.3
|
||||
aioshelly==0.5.4
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==1.2.1
|
||||
|
|
|
@ -91,7 +91,11 @@ async def coap_wrapper(hass):
|
|||
"""Setups a coap wrapper with mocked device."""
|
||||
await async_setup_component(hass, "shelly", {})
|
||||
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={})
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={"sleep_period": 0, "model": "SHSW-25"},
|
||||
unique_id="12345678",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
device = Mock(
|
||||
|
@ -99,6 +103,7 @@ async def coap_wrapper(hass):
|
|||
settings=MOCK_SETTINGS,
|
||||
shelly=MOCK_SHELLY,
|
||||
update=AsyncMock(),
|
||||
initialized=True,
|
||||
)
|
||||
|
||||
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
|
||||
|
|
|
@ -13,7 +13,8 @@ from tests.common import MockConfigEntry
|
|||
|
||||
MOCK_SETTINGS = {
|
||||
"name": "Test name",
|
||||
"device": {"mac": "test-mac", "hostname": "test-host"},
|
||||
"device": {"mac": "test-mac", "hostname": "test-host", "type": "SHSW-1"},
|
||||
"sleep_period": 0,
|
||||
}
|
||||
DISCOVERY_INFO = {
|
||||
"host": "1.1.1.1",
|
||||
|
@ -57,6 +58,8 @@ async def test_form(hass):
|
|||
assert result2["title"] == "Test name"
|
||||
assert result2["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"model": "SHSW-1",
|
||||
"sleep_period": 0,
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
@ -101,6 +104,8 @@ async def test_title_without_name(hass):
|
|||
assert result2["title"] == "shelly1pm-12345"
|
||||
assert result2["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"model": "SHSW-1",
|
||||
"sleep_period": 0,
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
@ -149,6 +154,8 @@ async def test_form_auth(hass):
|
|||
assert result3["title"] == "Test name"
|
||||
assert result3["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"model": "SHSW-1",
|
||||
"sleep_period": 0,
|
||||
"username": "test username",
|
||||
"password": "test password",
|
||||
}
|
||||
|
@ -369,6 +376,8 @@ async def test_zeroconf(hass):
|
|||
assert result2["title"] == "Test name"
|
||||
assert result2["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"model": "SHSW-1",
|
||||
"sleep_period": 0,
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
@ -502,6 +511,8 @@ async def test_zeroconf_require_auth(hass):
|
|||
assert result3["title"] == "Test name"
|
||||
assert result3["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"model": "SHSW-1",
|
||||
"sleep_period": 0,
|
||||
"username": "test username",
|
||||
"password": "test password",
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue