diff --git a/CODEOWNERS b/CODEOWNERS index f8640070f9e..1c94eb3f542 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -387,7 +387,7 @@ homeassistant/components/seven_segments/* @fabaff homeassistant/components/seventeentrack/* @bachya homeassistant/components/sharkiq/* @ajmarks homeassistant/components/shell_command/* @home-assistant/core -homeassistant/components/shelly/* @balloob @bieniu +homeassistant/components/shelly/* @balloob @bieniu @thecode homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/sighthound/* @robmarkcole diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 628ee5a53cc..8696a0a23b5 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -2,8 +2,8 @@ import asyncio from datetime import timedelta import logging +from socket import gethostbyname -import aiocoap import aioshelly import async_timeout @@ -14,44 +14,59 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator +from homeassistant.helpers import ( + aiohttp_client, + device_registry, + singleton, + update_coordinator, +) -from .const import COAP_CONTEXT, DATA_CONFIG_ENTRY, DOMAIN +from .const import DATA_CONFIG_ENTRY, DOMAIN PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Shelly component.""" - hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} - hass.data[DOMAIN][COAP_CONTEXT] = await aiocoap.Context.create_client_context() +@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() - async def shutdown_listener(*_): - """Home Assistant shutdown listener.""" - await hass.data[DOMAIN][COAP_CONTEXT].shutdown() + @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: {}} return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Shelly from a config entry.""" temperature_unit = "C" if hass.config.units.is_metric else "F" + + ip_address = await hass.async_add_executor_job(gethostbyname, entry.data[CONF_HOST]) + options = aioshelly.ConnectionOptions( - entry.data[CONF_HOST], + ip_address, entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), temperature_unit, ) - coap_context = hass.data[DOMAIN][COAP_CONTEXT] + coap_context = await get_coap_context(hass) try: - async with async_timeout.timeout(10): + async with async_timeout.timeout(5): device = await aioshelly.Device.create( aiohttp_client.async_get_clientsession(hass), coap_context, @@ -78,23 +93,35 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): def __init__(self, hass, entry, device: aioshelly.Device): """Initialize the Shelly device wrapper.""" + sleep_mode = device.settings.get("sleep_mode") + + if sleep_mode: + sleep_period = sleep_mode["period"] + if sleep_mode["unit"] == "h": + sleep_period *= 60 # hours to minutes + + update_interval = 1.2 * sleep_period * 60 # minutes to seconds + else: + update_interval = 2 * device.settings["coiot"]["update_period"] + super().__init__( hass, _LOGGER, name=device.settings["name"] or device.settings["device"]["hostname"], - update_interval=timedelta(seconds=5), + update_interval=timedelta(seconds=update_interval), ) self.hass = hass self.entry = entry self.device = device + self.device.subscribe_updates(self.async_set_updated_data) + async def _async_update_data(self): """Fetch data.""" - try: async with async_timeout.timeout(5): return await self.device.update() - except (aiocoap.error.Error, OSError) as err: + except OSError as err: raise update_coordinator.UpdateFailed("Error fetching data") from err @property @@ -122,6 +149,10 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): sw_version=self.device.settings["fw"], ) + def shutdown(self): + """Shutdown the wrapper.""" + self.device.shutdown() + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" @@ -134,6 +165,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) if unload_ok: - hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) + hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id).shutdown() return unload_ok diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 9076d5b7338..6f79475c0bf 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -1,8 +1,8 @@ """Config flow for Shelly integration.""" import asyncio import logging +from socket import gethostbyname -import aiocoap import aiohttp import aioshelly import async_timeout @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import aiohttp_client +from .__init__ import get_coap_context from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -37,10 +38,13 @@ async def validate_input(hass: core.HomeAssistant, host, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ + ip_address = await hass.async_add_executor_job(gethostbyname, host) + options = aioshelly.ConnectionOptions( - host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD) + ip_address, data.get(CONF_USERNAME), data.get(CONF_PASSWORD) ) - coap_context = await aiocoap.Context.create_client_context() + coap_context = await get_coap_context(hass) + async with async_timeout.timeout(5): device = await aioshelly.Device.create( aiohttp_client.async_get_clientsession(hass), @@ -48,7 +52,7 @@ async def validate_input(hass: core.HomeAssistant, host, data): options, ) - await coap_context.shutdown() + device.shutdown() # Return info that you want to store in the config entry. return { diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index b40ada04b30..c17fc28c378 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -1,5 +1,4 @@ """Constants for the Shelly integration.""" -COAP_CONTEXT = "coap_context" DATA_CONFIG_ENTRY = "config_entry" DOMAIN = "shelly" diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 98dcbfda568..82b2e445cb3 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/shelly", - "requirements": ["aioshelly==0.4.0"], + "requirements": ["aioshelly==0.5.0"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], - "codeowners": ["@balloob", "@bieniu"] + "codeowners": ["@balloob", "@bieniu", "@thecode"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7dd43a13e28..fdf0526290f 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.4.0 +aioshelly==0.5.0 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58df0e5f5e8..69057753ce1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.4.0 +aioshelly==0.5.0 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index ce0c380dd95..1796847bd74 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -43,7 +43,6 @@ async def test_form(hass): "aioshelly.Device.create", new=AsyncMock( return_value=Mock( - shutdown=AsyncMock(), settings=MOCK_SETTINGS, ) ), @@ -88,7 +87,6 @@ async def test_title_without_name_and_prefix(hass): "aioshelly.Device.create", new=AsyncMock( return_value=Mock( - shutdown=AsyncMock(), settings=settings, ) ), @@ -137,7 +135,6 @@ async def test_form_auth(hass): "aioshelly.Device.create", new=AsyncMock( return_value=Mock( - shutdown=AsyncMock(), settings=MOCK_SETTINGS, ) ), @@ -309,7 +306,6 @@ async def test_zeroconf(hass): "aioshelly.Device.create", new=AsyncMock( return_value=Mock( - shutdown=AsyncMock(), settings=MOCK_SETTINGS, ) ), @@ -466,7 +462,6 @@ async def test_zeroconf_require_auth(hass): "aioshelly.Device.create", new=AsyncMock( return_value=Mock( - shutdown=AsyncMock(), settings=MOCK_SETTINGS, ) ),