Use own CoAP lib and support for multicast updates (#42718)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Shay Levy 2020-11-02 17:46:34 +02:00 committed by GitHub
parent e5dee965f1
commit f45075eeb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 62 additions and 33 deletions

View File

@ -387,7 +387,7 @@ homeassistant/components/seven_segments/* @fabaff
homeassistant/components/seventeentrack/* @bachya homeassistant/components/seventeentrack/* @bachya
homeassistant/components/sharkiq/* @ajmarks homeassistant/components/sharkiq/* @ajmarks
homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shell_command/* @home-assistant/core
homeassistant/components/shelly/* @balloob @bieniu homeassistant/components/shelly/* @balloob @bieniu @thecode
homeassistant/components/shiftr/* @fabaff homeassistant/components/shiftr/* @fabaff
homeassistant/components/shodan/* @fabaff homeassistant/components/shodan/* @fabaff
homeassistant/components/sighthound/* @robmarkcole homeassistant/components/sighthound/* @robmarkcole

View File

@ -2,8 +2,8 @@
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
from socket import gethostbyname
import aiocoap
import aioshelly import aioshelly
import async_timeout import async_timeout
@ -14,44 +14,59 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady 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"] PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: dict): @singleton.singleton("shelly_coap")
"""Set up the Shelly component.""" async def get_coap_context(hass):
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} """Get CoAP context to be used in all Shelly devices."""
hass.data[DOMAIN][COAP_CONTEXT] = await aiocoap.Context.create_client_context() context = aioshelly.COAP()
await context.initialize()
async def shutdown_listener(*_): @callback
"""Home Assistant shutdown listener.""" def shutdown_listener(ev):
await hass.data[DOMAIN][COAP_CONTEXT].shutdown() context.close()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener) 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 return True
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."""
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])
options = aioshelly.ConnectionOptions( options = aioshelly.ConnectionOptions(
entry.data[CONF_HOST], ip_address,
entry.data.get(CONF_USERNAME), entry.data.get(CONF_USERNAME),
entry.data.get(CONF_PASSWORD), entry.data.get(CONF_PASSWORD),
temperature_unit, temperature_unit,
) )
coap_context = hass.data[DOMAIN][COAP_CONTEXT] coap_context = await get_coap_context(hass)
try: try:
async with async_timeout.timeout(10): async with async_timeout.timeout(5):
device = await aioshelly.Device.create( device = await aioshelly.Device.create(
aiohttp_client.async_get_clientsession(hass), aiohttp_client.async_get_clientsession(hass),
coap_context, coap_context,
@ -78,23 +93,35 @@ 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."""
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__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
name=device.settings["name"] or device.settings["device"]["hostname"], name=device.settings["name"] or device.settings["device"]["hostname"],
update_interval=timedelta(seconds=5), 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)
async def _async_update_data(self): async def _async_update_data(self):
"""Fetch data.""" """Fetch data."""
try: try:
async with async_timeout.timeout(5): async with async_timeout.timeout(5):
return await self.device.update() 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 raise update_coordinator.UpdateFailed("Error fetching data") from err
@property @property
@ -122,6 +149,10 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
sw_version=self.device.settings["fw"], sw_version=self.device.settings["fw"],
) )
def shutdown(self):
"""Shutdown the wrapper."""
self.device.shutdown()
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."""
@ -134,6 +165,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
) )
) )
if unload_ok: 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 return unload_ok

View File

@ -1,8 +1,8 @@
"""Config flow for Shelly integration.""" """Config flow for Shelly integration."""
import asyncio import asyncio
import logging import logging
from socket import gethostbyname
import aiocoap
import aiohttp import aiohttp
import aioshelly import aioshelly
import async_timeout import async_timeout
@ -17,6 +17,7 @@ from homeassistant.const import (
) )
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .__init__ import get_coap_context
from .const import DOMAIN # pylint:disable=unused-import from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__) _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. 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( 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): async with async_timeout.timeout(5):
device = await aioshelly.Device.create( device = await aioshelly.Device.create(
aiohttp_client.async_get_clientsession(hass), aiohttp_client.async_get_clientsession(hass),
@ -48,7 +52,7 @@ async def validate_input(hass: core.HomeAssistant, host, data):
options, options,
) )
await coap_context.shutdown() device.shutdown()
# Return info that you want to store in the config entry. # Return info that you want to store in the config entry.
return { return {

View File

@ -1,5 +1,4 @@
"""Constants for the Shelly integration.""" """Constants for the Shelly integration."""
COAP_CONTEXT = "coap_context"
DATA_CONFIG_ENTRY = "config_entry" DATA_CONFIG_ENTRY = "config_entry"
DOMAIN = "shelly" DOMAIN = "shelly"

View File

@ -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.4.0"], "requirements": ["aioshelly==0.5.0"],
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
"codeowners": ["@balloob", "@bieniu"] "codeowners": ["@balloob", "@bieniu", "@thecode"]
} }

View File

@ -221,7 +221,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3 aiopylgtv==0.3.3
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==0.4.0 aioshelly==0.5.0
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==1.2.1 aioswitcher==1.2.1

View File

@ -137,7 +137,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3 aiopylgtv==0.3.3
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==0.4.0 aioshelly==0.5.0
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==1.2.1 aioswitcher==1.2.1

View File

@ -43,7 +43,6 @@ async def test_form(hass):
"aioshelly.Device.create", "aioshelly.Device.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
shutdown=AsyncMock(),
settings=MOCK_SETTINGS, settings=MOCK_SETTINGS,
) )
), ),
@ -88,7 +87,6 @@ async def test_title_without_name_and_prefix(hass):
"aioshelly.Device.create", "aioshelly.Device.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
shutdown=AsyncMock(),
settings=settings, settings=settings,
) )
), ),
@ -137,7 +135,6 @@ async def test_form_auth(hass):
"aioshelly.Device.create", "aioshelly.Device.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
shutdown=AsyncMock(),
settings=MOCK_SETTINGS, settings=MOCK_SETTINGS,
) )
), ),
@ -309,7 +306,6 @@ async def test_zeroconf(hass):
"aioshelly.Device.create", "aioshelly.Device.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
shutdown=AsyncMock(),
settings=MOCK_SETTINGS, settings=MOCK_SETTINGS,
) )
), ),
@ -466,7 +462,6 @@ async def test_zeroconf_require_auth(hass):
"aioshelly.Device.create", "aioshelly.Device.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
shutdown=AsyncMock(),
settings=MOCK_SETTINGS, settings=MOCK_SETTINGS,
) )
), ),