mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
Add support for Shelly battery operated devices (#45406)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
fcc14933d0
commit
0875f654c8
@ -17,12 +17,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator
|
||||||
aiohttp_client,
|
|
||||||
device_registry,
|
|
||||||
singleton,
|
|
||||||
update_coordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
AIOSHELLY_DEVICE_TIMEOUT_SEC,
|
AIOSHELLY_DEVICE_TIMEOUT_SEC,
|
||||||
@ -32,36 +27,23 @@ from .const import (
|
|||||||
BATTERY_DEVICES_WITH_PERMANENT_CONNECTION,
|
BATTERY_DEVICES_WITH_PERMANENT_CONNECTION,
|
||||||
COAP,
|
COAP,
|
||||||
DATA_CONFIG_ENTRY,
|
DATA_CONFIG_ENTRY,
|
||||||
|
DEVICE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_SHELLY_CLICK,
|
EVENT_SHELLY_CLICK,
|
||||||
INPUTS_EVENTS_DICT,
|
INPUTS_EVENTS_DICT,
|
||||||
POLLING_TIMEOUT_MULTIPLIER,
|
POLLING_TIMEOUT_SEC,
|
||||||
REST,
|
REST,
|
||||||
REST_SENSORS_UPDATE_INTERVAL,
|
REST_SENSORS_UPDATE_INTERVAL,
|
||||||
SLEEP_PERIOD_MULTIPLIER,
|
SLEEP_PERIOD_MULTIPLIER,
|
||||||
UPDATE_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"]
|
PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"]
|
||||||
|
SLEEPING_PLATFORMS = ["binary_sensor", "sensor"]
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
"""Set up the Shelly component."""
|
"""Set up the Shelly component."""
|
||||||
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
|
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):
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Set up Shelly from a config entry."""
|
"""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"
|
temperature_unit = "C" if hass.config.units.is_metric else "F"
|
||||||
|
|
||||||
ip_address = await hass.async_add_executor_job(gethostbyname, entry.data[CONF_HOST])
|
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)
|
coap_context = await get_coap_context(hass)
|
||||||
|
|
||||||
try:
|
device = await aioshelly.Device.create(
|
||||||
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
|
aiohttp_client.async_get_clientsession(hass),
|
||||||
device = await aioshelly.Device.create(
|
coap_context,
|
||||||
aiohttp_client.async_get_clientsession(hass),
|
options,
|
||||||
coap_context,
|
False,
|
||||||
options,
|
)
|
||||||
)
|
|
||||||
except (asyncio.TimeoutError, OSError) as err:
|
|
||||||
raise ConfigEntryNotReady from err
|
|
||||||
|
|
||||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {}
|
dev_reg = await device_registry.async_get_registry(hass)
|
||||||
coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
|
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
|
COAP
|
||||||
] = ShellyDeviceWrapper(hass, entry, device)
|
] = ShellyDeviceWrapper(hass, entry, device)
|
||||||
await coap_wrapper.async_setup()
|
await device_wrapper.async_setup()
|
||||||
|
|
||||||
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
|
platforms = SLEEPING_PLATFORMS
|
||||||
REST
|
|
||||||
] = ShellyDeviceRestWrapper(hass, device)
|
|
||||||
|
|
||||||
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.async_create_task(
|
||||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
||||||
"""Wrapper for a Shelly device with Home Assistant specific functions."""
|
"""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):
|
def __init__(self, hass, entry, device: aioshelly.Device):
|
||||||
"""Initialize the Shelly device wrapper."""
|
"""Initialize the Shelly device wrapper."""
|
||||||
self.device_id = None
|
self.device_id = None
|
||||||
sleep_mode = device.settings.get("sleep_mode")
|
sleep_period = entry.data["sleep_period"]
|
||||||
|
|
||||||
if sleep_mode:
|
if sleep_period:
|
||||||
sleep_period = sleep_mode["period"]
|
update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period
|
||||||
if sleep_mode["unit"] == "h":
|
|
||||||
sleep_period *= 60 # hours to minutes
|
|
||||||
|
|
||||||
update_interval = (
|
|
||||||
SLEEP_PERIOD_MULTIPLIER * sleep_period * 60
|
|
||||||
) # minutes to seconds
|
|
||||||
else:
|
else:
|
||||||
update_interval = (
|
update_interval = (
|
||||||
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
|
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
device_name = get_device_name(device) if device.initialized else entry.title
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=get_device_name(device),
|
name=device_name,
|
||||||
update_interval=timedelta(seconds=update_interval),
|
update_interval=timedelta(seconds=update_interval),
|
||||||
)
|
)
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entry = entry
|
self.entry = entry
|
||||||
self.device = device
|
self.device = device
|
||||||
|
|
||||||
self.device.subscribe_updates(self.async_set_updated_data)
|
self._async_remove_device_updates_handler = self.async_add_listener(
|
||||||
|
self._async_device_updates_handler
|
||||||
self._async_remove_input_events_handler = self.async_add_listener(
|
|
||||||
self._async_input_events_handler
|
|
||||||
)
|
)
|
||||||
self._last_input_events_count = dict()
|
self._last_input_events_count = {}
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_input_events_handler(self):
|
def _async_device_updates_handler(self):
|
||||||
"""Handle device input events."""
|
"""Handle device updates."""
|
||||||
|
if not self.device.initialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for input events
|
||||||
for block in self.device.blocks:
|
for block in self.device.blocks:
|
||||||
if (
|
if (
|
||||||
"inputEvent" not in block.sensor_ids
|
"inputEvent" not in block.sensor_ids
|
||||||
@ -192,13 +220,9 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||||||
|
|
||||||
async def _async_update_data(self):
|
async def _async_update_data(self):
|
||||||
"""Fetch data."""
|
"""Fetch data."""
|
||||||
|
|
||||||
_LOGGER.debug("Polling Shelly Device - %s", self.name)
|
_LOGGER.debug("Polling Shelly Device - %s", self.name)
|
||||||
try:
|
try:
|
||||||
async with async_timeout.timeout(
|
async with async_timeout.timeout(POLLING_TIMEOUT_SEC):
|
||||||
POLLING_TIMEOUT_MULTIPLIER
|
|
||||||
* self.device.settings["coiot"]["update_period"]
|
|
||||||
):
|
|
||||||
return await self.device.update()
|
return await self.device.update()
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise update_coordinator.UpdateFailed("Error fetching data") from err
|
raise update_coordinator.UpdateFailed("Error fetching data") from err
|
||||||
@ -206,18 +230,17 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self):
|
||||||
"""Model of the device."""
|
"""Model of the device."""
|
||||||
return self.device.settings["device"]["type"]
|
return self.entry.data["model"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mac(self):
|
def mac(self):
|
||||||
"""Mac address of the device."""
|
"""Mac address of the device."""
|
||||||
return self.device.settings["device"]["mac"]
|
return self.entry.unique_id
|
||||||
|
|
||||||
async def async_setup(self):
|
async def async_setup(self):
|
||||||
"""Set up the wrapper."""
|
"""Set up the wrapper."""
|
||||||
|
|
||||||
dev_reg = await device_registry.async_get_registry(self.hass)
|
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(
|
entry = dev_reg.async_get_or_create(
|
||||||
config_entry_id=self.entry.entry_id,
|
config_entry_id=self.entry.entry_id,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
@ -225,15 +248,16 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
|
|||||||
# This is duplicate but otherwise via_device can't work
|
# This is duplicate but otherwise via_device can't work
|
||||||
identifiers={(DOMAIN, self.mac)},
|
identifiers={(DOMAIN, self.mac)},
|
||||||
manufacturer="Shelly",
|
manufacturer="Shelly",
|
||||||
model=aioshelly.MODEL_NAMES.get(model_type, model_type),
|
model=aioshelly.MODEL_NAMES.get(self.model, self.model),
|
||||||
sw_version=self.device.settings["fw"],
|
sw_version=sw_version,
|
||||||
)
|
)
|
||||||
self.device_id = entry.id
|
self.device_id = entry.id
|
||||||
|
self.device.subscribe_updates(self.async_set_updated_data)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""Shutdown the wrapper."""
|
"""Shutdown the wrapper."""
|
||||||
self.device.shutdown()
|
self.device.shutdown()
|
||||||
self._async_remove_input_events_handler()
|
self._async_remove_device_updates_handler()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_ha_stop(self, _):
|
def _handle_ha_stop(self, _):
|
||||||
@ -282,11 +306,23 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator):
|
|||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Unload a config entry."""
|
"""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(
|
unload_ok = all(
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
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_PROBLEM,
|
||||||
DEVICE_CLASS_SMOKE,
|
DEVICE_CLASS_SMOKE,
|
||||||
DEVICE_CLASS_VIBRATION,
|
DEVICE_CLASS_VIBRATION,
|
||||||
|
STATE_ON,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ from .entity import (
|
|||||||
RestAttributeDescription,
|
RestAttributeDescription,
|
||||||
ShellyBlockAttributeEntity,
|
ShellyBlockAttributeEntity,
|
||||||
ShellyRestAttributeEntity,
|
ShellyRestAttributeEntity,
|
||||||
|
ShellySleepingBlockAttributeEntity,
|
||||||
async_setup_entry_attribute_entities,
|
async_setup_entry_attribute_entities,
|
||||||
async_setup_entry_rest,
|
async_setup_entry_rest,
|
||||||
)
|
)
|
||||||
@ -98,13 +100,25 @@ REST_SENSORS = {
|
|||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up sensors for device."""
|
"""Set up sensors for device."""
|
||||||
await async_setup_entry_attribute_entities(
|
if config_entry.data["sleep_period"]:
|
||||||
hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor
|
await async_setup_entry_attribute_entities(
|
||||||
)
|
hass,
|
||||||
|
config_entry,
|
||||||
await async_setup_entry_rest(
|
async_add_entities,
|
||||||
hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestBinarySensor
|
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):
|
class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity):
|
||||||
@ -123,3 +137,17 @@ class ShellyRestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity):
|
|||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if REST sensor state is on."""
|
"""Return true if REST sensor state is on."""
|
||||||
return bool(self.attribute_value)
|
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 homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
from . import get_coap_context
|
|
||||||
from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC
|
from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC
|
||||||
from .const import DOMAIN # pylint:disable=unused-import
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
from .utils import get_coap_context, get_device_sleep_period
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -53,6 +53,8 @@ async def validate_input(hass: core.HomeAssistant, host, data):
|
|||||||
return {
|
return {
|
||||||
"title": device.settings["name"],
|
"title": device.settings["name"],
|
||||||
"hostname": device.settings["device"]["hostname"],
|
"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:
|
else:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=device_info["title"] or device_info["hostname"],
|
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(
|
return self.async_show_form(
|
||||||
@ -121,7 +127,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=device_info["title"] or device_info["hostname"],
|
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:
|
else:
|
||||||
user_input = {}
|
user_input = {}
|
||||||
@ -172,7 +183,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=device_info["title"] or device_info["hostname"],
|
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(
|
return self.async_show_form(
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
COAP = "coap"
|
COAP = "coap"
|
||||||
DATA_CONFIG_ENTRY = "config_entry"
|
DATA_CONFIG_ENTRY = "config_entry"
|
||||||
|
DEVICE = "device"
|
||||||
DOMAIN = "shelly"
|
DOMAIN = "shelly"
|
||||||
REST = "rest"
|
REST = "rest"
|
||||||
|
|
||||||
# Used to calculate the timeout in "_async_update_data" used for polling data from devices.
|
# Used in "_async_update_data" as timeout for polling data from devices.
|
||||||
POLLING_TIMEOUT_MULTIPLIER = 1.2
|
POLLING_TIMEOUT_SEC = 18
|
||||||
|
|
||||||
# Refresh interval for REST sensors
|
# Refresh interval for REST sensors
|
||||||
REST_SENSORS_UPDATE_INTERVAL = 60
|
REST_SENSORS_UPDATE_INTERVAL = 60
|
||||||
|
@ -1,24 +1,49 @@
|
|||||||
"""Shelly entity helper."""
|
"""Shelly entity helper."""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
from typing import Any, Callable, Optional, Union
|
from typing import Any, Callable, Optional, Union
|
||||||
|
|
||||||
import aioshelly
|
import aioshelly
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import callback
|
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 . import ShellyDeviceRestWrapper, ShellyDeviceWrapper
|
||||||
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST
|
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST
|
||||||
from .utils import async_remove_shelly_entity, get_entity_name
|
from .utils import async_remove_shelly_entity, get_entity_name
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry_attribute_entities(
|
async def async_setup_entry_attribute_entities(
|
||||||
hass, config_entry, async_add_entities, sensors, sensor_class
|
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][
|
wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
|
||||||
config_entry.entry_id
|
config_entry.entry_id
|
||||||
][COAP]
|
][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 = []
|
blocks = []
|
||||||
|
|
||||||
for block in wrapper.device.blocks:
|
for block in wrapper.device.blocks:
|
||||||
@ -36,9 +61,7 @@ async def async_setup_entry_attribute_entities(
|
|||||||
wrapper.device.settings, block
|
wrapper.device.settings, block
|
||||||
):
|
):
|
||||||
domain = sensor_class.__module__.split(".")[-1]
|
domain = sensor_class.__module__.split(".")[-1]
|
||||||
unique_id = sensor_class(
|
unique_id = f"{wrapper.mac}-{block.description}-{sensor_id}"
|
||||||
wrapper, block, sensor_id, description
|
|
||||||
).unique_id
|
|
||||||
await async_remove_shelly_entity(hass, domain, unique_id)
|
await async_remove_shelly_entity(hass, domain, unique_id)
|
||||||
else:
|
else:
|
||||||
blocks.append((block, sensor_id, description))
|
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(
|
async def async_setup_entry_rest(
|
||||||
hass, config_entry, async_add_entities, sensors, sensor_class
|
hass, config_entry, async_add_entities, sensors, sensor_class
|
||||||
):
|
):
|
||||||
@ -163,7 +219,7 @@ class ShellyBlockEntity(entity.Entity):
|
|||||||
|
|
||||||
|
|
||||||
class ShellyBlockAttributeEntity(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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -176,12 +232,11 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
|
|||||||
super().__init__(wrapper, block)
|
super().__init__(wrapper, block)
|
||||||
self.attribute = attribute
|
self.attribute = attribute
|
||||||
self.description = description
|
self.description = description
|
||||||
self.info = block.info(attribute)
|
|
||||||
|
|
||||||
unit = self.description.unit
|
unit = self.description.unit
|
||||||
|
|
||||||
if callable(unit):
|
if callable(unit):
|
||||||
unit = unit(self.info)
|
unit = unit(block.info(attribute))
|
||||||
|
|
||||||
self._unit = unit
|
self._unit = unit
|
||||||
self._unique_id = f"{super().unique_id}-{self.attribute}"
|
self._unique_id = f"{super().unique_id}-{self.attribute}"
|
||||||
@ -320,3 +375,67 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return self.description.device_state_attributes(self.wrapper.device.status)
|
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",
|
"name": "Shelly",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
"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*" }],
|
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
|
||||||
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"]
|
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"]
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ from .entity import (
|
|||||||
RestAttributeDescription,
|
RestAttributeDescription,
|
||||||
ShellyBlockAttributeEntity,
|
ShellyBlockAttributeEntity,
|
||||||
ShellyRestAttributeEntity,
|
ShellyRestAttributeEntity,
|
||||||
|
ShellySleepingBlockAttributeEntity,
|
||||||
async_setup_entry_attribute_entities,
|
async_setup_entry_attribute_entities,
|
||||||
async_setup_entry_rest,
|
async_setup_entry_rest,
|
||||||
)
|
)
|
||||||
@ -185,12 +186,17 @@ REST_SENSORS = {
|
|||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up sensors for device."""
|
"""Set up sensors for device."""
|
||||||
await async_setup_entry_attribute_entities(
|
if config_entry.data["sleep_period"]:
|
||||||
hass, config_entry, async_add_entities, SENSORS, ShellySensor
|
await async_setup_entry_attribute_entities(
|
||||||
)
|
hass, config_entry, async_add_entities, SENSORS, ShellySleepingSensor
|
||||||
await async_setup_entry_rest(
|
)
|
||||||
hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestSensor
|
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):
|
class ShellySensor(ShellyBlockAttributeEntity):
|
||||||
@ -209,3 +215,15 @@ class ShellyRestSensor(ShellyRestAttributeEntity):
|
|||||||
def state(self):
|
def state(self):
|
||||||
"""Return value of sensor."""
|
"""Return value of sensor."""
|
||||||
return self.attribute_value
|
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
|
import aioshelly
|
||||||
|
|
||||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import singleton
|
||||||
from homeassistant.util.dt import parse_datetime, utcnow
|
from homeassistant.util.dt import parse_datetime, utcnow
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -182,3 +183,30 @@ def get_device_wrapper(hass: HomeAssistant, device_id: str):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return None
|
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
|
aiorecollect==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==0.5.3
|
aioshelly==0.5.4
|
||||||
|
|
||||||
# homeassistant.components.switcher_kis
|
# homeassistant.components.switcher_kis
|
||||||
aioswitcher==1.2.1
|
aioswitcher==1.2.1
|
||||||
|
@ -137,7 +137,7 @@ aiopylgtv==0.3.3
|
|||||||
aiorecollect==1.0.1
|
aiorecollect==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==0.5.3
|
aioshelly==0.5.4
|
||||||
|
|
||||||
# homeassistant.components.switcher_kis
|
# homeassistant.components.switcher_kis
|
||||||
aioswitcher==1.2.1
|
aioswitcher==1.2.1
|
||||||
|
@ -91,7 +91,11 @@ async def coap_wrapper(hass):
|
|||||||
"""Setups a coap wrapper with mocked device."""
|
"""Setups a coap wrapper with mocked device."""
|
||||||
await async_setup_component(hass, "shelly", {})
|
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)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
device = Mock(
|
device = Mock(
|
||||||
@ -99,6 +103,7 @@ async def coap_wrapper(hass):
|
|||||||
settings=MOCK_SETTINGS,
|
settings=MOCK_SETTINGS,
|
||||||
shelly=MOCK_SHELLY,
|
shelly=MOCK_SHELLY,
|
||||||
update=AsyncMock(),
|
update=AsyncMock(),
|
||||||
|
initialized=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
|
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
|
||||||
|
@ -13,7 +13,8 @@ from tests.common import MockConfigEntry
|
|||||||
|
|
||||||
MOCK_SETTINGS = {
|
MOCK_SETTINGS = {
|
||||||
"name": "Test name",
|
"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 = {
|
DISCOVERY_INFO = {
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
@ -57,6 +58,8 @@ async def test_form(hass):
|
|||||||
assert result2["title"] == "Test name"
|
assert result2["title"] == "Test name"
|
||||||
assert result2["data"] == {
|
assert result2["data"] == {
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
|
"model": "SHSW-1",
|
||||||
|
"sleep_period": 0,
|
||||||
}
|
}
|
||||||
assert len(mock_setup.mock_calls) == 1
|
assert len(mock_setup.mock_calls) == 1
|
||||||
assert len(mock_setup_entry.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["title"] == "shelly1pm-12345"
|
||||||
assert result2["data"] == {
|
assert result2["data"] == {
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
|
"model": "SHSW-1",
|
||||||
|
"sleep_period": 0,
|
||||||
}
|
}
|
||||||
assert len(mock_setup.mock_calls) == 1
|
assert len(mock_setup.mock_calls) == 1
|
||||||
assert len(mock_setup_entry.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["title"] == "Test name"
|
||||||
assert result3["data"] == {
|
assert result3["data"] == {
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
|
"model": "SHSW-1",
|
||||||
|
"sleep_period": 0,
|
||||||
"username": "test username",
|
"username": "test username",
|
||||||
"password": "test password",
|
"password": "test password",
|
||||||
}
|
}
|
||||||
@ -369,6 +376,8 @@ async def test_zeroconf(hass):
|
|||||||
assert result2["title"] == "Test name"
|
assert result2["title"] == "Test name"
|
||||||
assert result2["data"] == {
|
assert result2["data"] == {
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
|
"model": "SHSW-1",
|
||||||
|
"sleep_period": 0,
|
||||||
}
|
}
|
||||||
assert len(mock_setup.mock_calls) == 1
|
assert len(mock_setup.mock_calls) == 1
|
||||||
assert len(mock_setup_entry.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["title"] == "Test name"
|
||||||
assert result3["data"] == {
|
assert result3["data"] == {
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
|
"model": "SHSW-1",
|
||||||
|
"sleep_period": 0,
|
||||||
"username": "test username",
|
"username": "test username",
|
||||||
"password": "test password",
|
"password": "test password",
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user