2018-08-21 19:25:16 +00:00
|
|
|
"""Access point for the HomematicIP Cloud component."""
|
2021-08-21 18:19:56 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2018-07-06 21:05:34 +00:00
|
|
|
import asyncio
|
2021-09-29 12:06:51 +00:00
|
|
|
from collections.abc import Callable
|
2018-07-06 21:05:34 +00:00
|
|
|
import logging
|
2021-09-29 12:06:51 +00:00
|
|
|
from typing import Any
|
2018-07-06 21:05:34 +00:00
|
|
|
|
2019-04-23 23:47:31 +00:00
|
|
|
from homematicip.aio.auth import AsyncAuth
|
|
|
|
from homematicip.aio.home import AsyncHome
|
|
|
|
from homematicip.base.base_connection import HmipConnectionError
|
2019-09-28 20:34:14 +00:00
|
|
|
from homematicip.base.enums import EventType
|
2019-04-23 23:47:31 +00:00
|
|
|
|
2019-04-25 22:13:07 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2021-04-23 07:49:02 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2019-02-14 15:01:46 +00:00
|
|
|
from homeassistant.exceptions import ConfigEntryNotReady
|
2018-08-21 19:25:16 +00:00
|
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
2018-07-06 21:05:34 +00:00
|
|
|
|
2021-03-02 20:43:59 +00:00
|
|
|
from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN, PLATFORMS
|
2018-07-06 21:05:34 +00:00
|
|
|
from .errors import HmipcConnectionError
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class HomematicipAuth:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Manages HomematicIP client registration."""
|
|
|
|
|
2021-08-21 18:19:56 +00:00
|
|
|
auth: AsyncAuth
|
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
def __init__(self, hass, config) -> None:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Initialize HomematicIP Cloud client registration."""
|
|
|
|
self.hass = hass
|
|
|
|
self.config = config
|
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
async def async_setup(self) -> bool:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Connect to HomematicIP for registration."""
|
|
|
|
try:
|
|
|
|
self.auth = await self.get_auth(
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass, self.config.get(HMIPC_HAPID), self.config.get(HMIPC_PIN)
|
2018-07-06 21:05:34 +00:00
|
|
|
)
|
2020-09-03 07:52:51 +00:00
|
|
|
return self.auth is not None
|
2018-07-06 21:05:34 +00:00
|
|
|
except HmipcConnectionError:
|
|
|
|
return False
|
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
async def async_checkbutton(self) -> bool:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Check blue butten has been pressed."""
|
|
|
|
try:
|
2019-02-19 17:53:20 +00:00
|
|
|
return await self.auth.isRequestAcknowledged()
|
2018-07-06 21:05:34 +00:00
|
|
|
except HmipConnectionError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
async def async_register(self):
|
|
|
|
"""Register client at HomematicIP."""
|
|
|
|
try:
|
|
|
|
authtoken = await self.auth.requestAuthToken()
|
|
|
|
await self.auth.confirmAuthToken(authtoken)
|
|
|
|
return authtoken
|
|
|
|
except HmipConnectionError:
|
|
|
|
return False
|
|
|
|
|
2021-04-23 07:49:02 +00:00
|
|
|
async def get_auth(self, hass: HomeAssistant, hapid, pin):
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Create a HomematicIP access point object."""
|
|
|
|
auth = AsyncAuth(hass.loop, async_get_clientsession(hass))
|
|
|
|
try:
|
|
|
|
await auth.init(hapid)
|
|
|
|
if pin:
|
|
|
|
auth.pin = pin
|
2019-07-31 19:25:30 +00:00
|
|
|
await auth.connectionRequest("HomeAssistant")
|
2018-07-06 21:05:34 +00:00
|
|
|
except HmipConnectionError:
|
2020-09-03 07:52:51 +00:00
|
|
|
return None
|
2018-07-06 21:05:34 +00:00
|
|
|
return auth
|
|
|
|
|
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class HomematicipHAP:
|
2018-08-21 19:25:16 +00:00
|
|
|
"""Manages HomematicIP HTTP and WebSocket connection."""
|
2018-07-06 21:05:34 +00:00
|
|
|
|
2021-08-21 18:19:56 +00:00
|
|
|
home: AsyncHome
|
|
|
|
|
2021-04-23 07:49:02 +00:00
|
|
|
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
2018-08-21 19:25:16 +00:00
|
|
|
"""Initialize HomematicIP Cloud connection."""
|
2018-07-06 21:05:34 +00:00
|
|
|
self.hass = hass
|
|
|
|
self.config_entry = config_entry
|
|
|
|
|
|
|
|
self._ws_close_requested = False
|
2021-08-21 18:19:56 +00:00
|
|
|
self._retry_task: asyncio.Task | None = None
|
2018-07-06 21:05:34 +00:00
|
|
|
self._tries = 0
|
|
|
|
self._accesspoint_connected = True
|
2021-08-21 18:19:56 +00:00
|
|
|
self.hmip_device_by_entity_id: dict[str, Any] = {}
|
|
|
|
self.reset_connection_listener: Callable | None = None
|
2018-07-06 21:05:34 +00:00
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
async def async_setup(self, tries: int = 0) -> bool:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Initialize connection."""
|
|
|
|
try:
|
|
|
|
self.home = await self.get_hap(
|
|
|
|
self.hass,
|
|
|
|
self.config_entry.data.get(HMIPC_HAPID),
|
|
|
|
self.config_entry.data.get(HMIPC_AUTHTOKEN),
|
2019-07-31 19:25:30 +00:00
|
|
|
self.config_entry.data.get(HMIPC_NAME),
|
2018-07-06 21:05:34 +00:00
|
|
|
)
|
2020-08-28 11:50:32 +00:00
|
|
|
except HmipcConnectionError as err:
|
|
|
|
raise ConfigEntryNotReady from err
|
2020-02-16 09:09:26 +00:00
|
|
|
except Exception as err: # pylint: disable=broad-except
|
|
|
|
_LOGGER.error("Error connecting with HomematicIP Cloud: %s", err)
|
|
|
|
return False
|
2018-07-06 21:05:34 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info(
|
2020-01-26 13:54:33 +00:00
|
|
|
"Connected to HomematicIP with HAP %s", self.config_entry.unique_id
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-07-06 21:05:34 +00:00
|
|
|
|
2023-01-25 09:01:51 +00:00
|
|
|
await self.hass.config_entries.async_forward_entry_setups(
|
|
|
|
self.config_entry, PLATFORMS
|
|
|
|
)
|
2021-04-27 14:09:59 +00:00
|
|
|
|
2018-07-06 21:05:34 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
@callback
|
2019-11-25 13:17:14 +00:00
|
|
|
def async_update(self, *args, **kwargs) -> None:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Async update the home device.
|
|
|
|
|
2018-08-21 19:25:16 +00:00
|
|
|
Triggered when the HMIP HOME_CHANGED event has fired.
|
2018-07-06 21:05:34 +00:00
|
|
|
There are several occasions for this event to happen.
|
2019-08-03 16:49:34 +00:00
|
|
|
1. We are interested to check whether the access point
|
2020-08-25 23:55:55 +00:00
|
|
|
is still connected. If not, entity state changes cannot
|
2018-07-06 21:05:34 +00:00
|
|
|
be forwarded to hass. So if access point is disconnected all devices
|
|
|
|
are set to unavailable.
|
2019-08-03 16:49:34 +00:00
|
|
|
2. We need to update home including devices and groups after a reconnect.
|
|
|
|
3. We need to update home without devices and groups in all other cases.
|
|
|
|
|
2018-07-06 21:05:34 +00:00
|
|
|
"""
|
|
|
|
if not self.home.connected:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("HMIP access point has lost connection with the cloud")
|
2018-07-06 21:05:34 +00:00
|
|
|
self._accesspoint_connected = False
|
|
|
|
self.set_all_to_unavailable()
|
|
|
|
elif not self._accesspoint_connected:
|
2018-12-25 08:40:36 +00:00
|
|
|
# Now the HOME_CHANGED event has fired indicating the access
|
|
|
|
# point has reconnected to the cloud again.
|
2020-08-25 23:55:55 +00:00
|
|
|
# Explicitly getting an update as entity states might have
|
2018-07-06 21:05:34 +00:00
|
|
|
# changed during access point disconnect."""
|
|
|
|
|
2018-10-02 09:03:09 +00:00
|
|
|
job = self.hass.async_create_task(self.get_state())
|
2018-07-06 21:05:34 +00:00
|
|
|
job.add_done_callback(self.get_state_finished)
|
2019-08-03 16:49:34 +00:00
|
|
|
self._accesspoint_connected = True
|
2018-07-06 21:05:34 +00:00
|
|
|
|
2019-09-28 20:34:14 +00:00
|
|
|
@callback
|
2019-11-25 13:17:14 +00:00
|
|
|
def async_create_entity(self, *args, **kwargs) -> None:
|
2020-08-25 23:55:55 +00:00
|
|
|
"""Create an entity or a group."""
|
2019-09-28 20:34:14 +00:00
|
|
|
is_device = EventType(kwargs["event_type"]) == EventType.DEVICE_ADDED
|
|
|
|
self.hass.async_create_task(self.async_create_entity_lazy(is_device))
|
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
async def async_create_entity_lazy(self, is_device=True) -> None:
|
2019-09-28 20:34:14 +00:00
|
|
|
"""Delay entity creation to allow the user to enter a device name."""
|
|
|
|
if is_device:
|
|
|
|
await asyncio.sleep(30)
|
|
|
|
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
async def get_state(self) -> None:
|
2018-08-21 19:25:16 +00:00
|
|
|
"""Update HMIP state and tell Home Assistant."""
|
2018-07-06 21:05:34 +00:00
|
|
|
await self.home.get_current_state()
|
|
|
|
self.update_all()
|
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
def get_state_finished(self, future) -> None:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Execute when get_state coroutine has finished."""
|
|
|
|
try:
|
|
|
|
future.result()
|
|
|
|
except HmipConnectionError:
|
|
|
|
# Somehow connection could not recover. Will disconnect and
|
|
|
|
# so reconnect loop is taking over.
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Updating state after HMIP access point reconnect failed")
|
2018-10-02 09:03:09 +00:00
|
|
|
self.hass.async_create_task(self.home.disable_events())
|
2018-07-06 21:05:34 +00:00
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
def set_all_to_unavailable(self) -> None:
|
2018-08-21 19:25:16 +00:00
|
|
|
"""Set all devices to unavailable and tell Home Assistant."""
|
2018-07-06 21:05:34 +00:00
|
|
|
for device in self.home.devices:
|
|
|
|
device.unreach = True
|
|
|
|
self.update_all()
|
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
def update_all(self) -> None:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Signal all devices to update their state."""
|
|
|
|
for device in self.home.devices:
|
|
|
|
device.fire_update_event()
|
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
async def async_connect(self) -> None:
|
2018-08-21 19:25:16 +00:00
|
|
|
"""Start WebSocket connection."""
|
2018-07-06 21:05:34 +00:00
|
|
|
tries = 0
|
|
|
|
while True:
|
2018-12-25 08:40:36 +00:00
|
|
|
retry_delay = 2 ** min(tries, 8)
|
|
|
|
|
2018-07-06 21:05:34 +00:00
|
|
|
try:
|
|
|
|
await self.home.get_current_state()
|
|
|
|
hmip_events = await self.home.enable_events()
|
|
|
|
tries = 0
|
|
|
|
await hmip_events
|
|
|
|
except HmipConnectionError:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error(
|
2022-12-22 10:38:59 +00:00
|
|
|
(
|
|
|
|
"Error connecting to HomematicIP with HAP %s. "
|
|
|
|
"Retrying in %d seconds"
|
|
|
|
),
|
2020-01-26 13:54:33 +00:00
|
|
|
self.config_entry.unique_id,
|
2019-07-31 19:25:30 +00:00
|
|
|
retry_delay,
|
|
|
|
)
|
2018-07-06 21:05:34 +00:00
|
|
|
|
|
|
|
if self._ws_close_requested:
|
|
|
|
break
|
|
|
|
self._ws_close_requested = False
|
|
|
|
tries += 1
|
2018-12-25 08:40:36 +00:00
|
|
|
|
2018-07-06 21:05:34 +00:00
|
|
|
try:
|
2019-07-31 19:25:30 +00:00
|
|
|
self._retry_task = self.hass.async_create_task(
|
|
|
|
asyncio.sleep(retry_delay)
|
|
|
|
)
|
2018-07-06 21:05:34 +00:00
|
|
|
await self._retry_task
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
break
|
|
|
|
|
2019-11-25 13:17:14 +00:00
|
|
|
async def async_reset(self) -> bool:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Close the websocket connection."""
|
|
|
|
self._ws_close_requested = True
|
|
|
|
if self._retry_task is not None:
|
|
|
|
self._retry_task.cancel()
|
2018-12-25 08:40:36 +00:00
|
|
|
await self.home.disable_events()
|
2018-08-21 19:25:16 +00:00
|
|
|
_LOGGER.info("Closed connection to HomematicIP cloud server")
|
2021-04-27 14:09:59 +00:00
|
|
|
await self.hass.config_entries.async_unload_platforms(
|
|
|
|
self.config_entry, PLATFORMS
|
|
|
|
)
|
2019-10-12 19:45:11 +00:00
|
|
|
self.hmip_device_by_entity_id = {}
|
2018-07-06 21:05:34 +00:00
|
|
|
return True
|
|
|
|
|
2020-02-03 19:27:20 +00:00
|
|
|
@callback
|
|
|
|
def shutdown(self, event) -> None:
|
|
|
|
"""Wrap the call to async_reset.
|
|
|
|
|
|
|
|
Used as an argument to EventBus.async_listen_once.
|
|
|
|
"""
|
|
|
|
self.hass.async_create_task(self.async_reset())
|
|
|
|
_LOGGER.debug(
|
|
|
|
"Reset connection to access point id %s", self.config_entry.unique_id
|
|
|
|
)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def get_hap(
|
2021-08-21 18:19:56 +00:00
|
|
|
self,
|
|
|
|
hass: HomeAssistant,
|
|
|
|
hapid: str | None,
|
|
|
|
authtoken: str | None,
|
|
|
|
name: str | None,
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> AsyncHome:
|
2018-07-06 21:05:34 +00:00
|
|
|
"""Create a HomematicIP access point object."""
|
|
|
|
home = AsyncHome(hass.loop, async_get_clientsession(hass))
|
|
|
|
|
|
|
|
home.name = name
|
2020-11-12 09:33:01 +00:00
|
|
|
# Use the title of the config entry as title for the home.
|
|
|
|
home.label = self.config_entry.title
|
|
|
|
home.modelType = "HomematicIP Cloud Home"
|
2018-07-06 21:05:34 +00:00
|
|
|
|
|
|
|
home.set_auth_token(authtoken)
|
|
|
|
try:
|
|
|
|
await home.init(hapid)
|
|
|
|
await home.get_current_state()
|
2020-08-28 11:50:32 +00:00
|
|
|
except HmipConnectionError as err:
|
|
|
|
raise HmipcConnectionError from err
|
2018-07-06 21:05:34 +00:00
|
|
|
home.on_update(self.async_update)
|
2019-09-28 20:34:14 +00:00
|
|
|
home.on_create(self.async_create_entity)
|
2018-07-06 21:05:34 +00:00
|
|
|
hass.loop.create_task(self.async_connect())
|
|
|
|
|
|
|
|
return home
|