2018-03-30 03:15:40 +00:00
|
|
|
"""Code to handle a Hue bridge."""
|
2021-05-17 15:07:25 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2018-03-30 03:15:40 +00:00
|
|
|
import asyncio
|
2021-11-16 19:59:17 +00:00
|
|
|
from collections.abc import Callable
|
2020-02-27 20:53:36 +00:00
|
|
|
import logging
|
2021-11-16 19:59:17 +00:00
|
|
|
from typing import Any
|
2018-03-30 03:15:40 +00:00
|
|
|
|
2020-02-08 21:20:37 +00:00
|
|
|
from aiohttp import client_exceptions
|
2021-11-16 19:59:17 +00:00
|
|
|
from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized
|
2021-12-23 13:04:10 +00:00
|
|
|
from aiohue.errors import AiohueException, BridgeBusy
|
2018-03-30 03:15:40 +00:00
|
|
|
import async_timeout
|
|
|
|
|
2019-12-16 18:45:09 +00:00
|
|
|
from homeassistant import core
|
2021-11-16 19:59:17 +00:00
|
|
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
2021-12-06 03:10:07 +00:00
|
|
|
from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform
|
2019-02-14 04:36:06 +00:00
|
|
|
from homeassistant.exceptions import ConfigEntryNotReady
|
2021-04-13 16:31:23 +00:00
|
|
|
from homeassistant.helpers import aiohttp_client
|
2018-03-30 03:15:40 +00:00
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
from .const import CONF_API_VERSION, DOMAIN
|
|
|
|
from .v1.sensor_base import SensorManager
|
|
|
|
from .v2.device import async_setup_devices
|
|
|
|
from .v2.hue_event import async_setup_hue_events
|
2018-03-30 03:15:40 +00:00
|
|
|
|
2020-02-08 21:20:37 +00:00
|
|
|
# How long should we sleep if the hub is busy
|
2020-02-27 20:53:36 +00:00
|
|
|
HUB_BUSY_SLEEP = 0.5
|
2021-04-27 14:09:59 +00:00
|
|
|
|
2021-12-06 03:10:07 +00:00
|
|
|
PLATFORMS_v1 = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR]
|
|
|
|
PLATFORMS_v2 = [
|
|
|
|
Platform.BINARY_SENSOR,
|
|
|
|
Platform.LIGHT,
|
|
|
|
Platform.SCENE,
|
|
|
|
Platform.SENSOR,
|
|
|
|
Platform.SWITCH,
|
|
|
|
]
|
2018-03-30 03:15:40 +00:00
|
|
|
|
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class HueBridge:
|
2018-03-30 03:15:40 +00:00
|
|
|
"""Manages a single Hue bridge."""
|
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
def __init__(self, hass: core.HomeAssistant, config_entry: ConfigEntry) -> None:
|
2018-03-30 03:15:40 +00:00
|
|
|
"""Initialize the system."""
|
|
|
|
self.config_entry = config_entry
|
|
|
|
self.hass = hass
|
2019-10-28 15:45:08 +00:00
|
|
|
self.authorized = False
|
2020-01-31 22:47:40 +00:00
|
|
|
# Jobs to be executed when API is reset.
|
2021-11-16 19:59:17 +00:00
|
|
|
self.reset_jobs: list[core.CALLBACK_TYPE] = []
|
|
|
|
self.sensor_manager: SensorManager | None = None
|
|
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
# store actual api connection to bridge as api
|
|
|
|
app_key: str = self.config_entry.data[CONF_API_KEY]
|
|
|
|
websession = aiohttp_client.async_get_clientsession(hass)
|
|
|
|
if self.api_version == 1:
|
|
|
|
self.api = HueBridgeV1(self.host, app_key, websession)
|
|
|
|
else:
|
|
|
|
self.api = HueBridgeV2(self.host, app_key, websession)
|
|
|
|
# store (this) bridge object in hass data
|
|
|
|
hass.data.setdefault(DOMAIN, {})[self.config_entry.entry_id] = self
|
2018-03-30 03:15:40 +00:00
|
|
|
|
2018-04-01 16:03:01 +00:00
|
|
|
@property
|
2021-11-16 19:59:17 +00:00
|
|
|
def host(self) -> str:
|
2018-04-01 16:03:01 +00:00
|
|
|
"""Return the host of this bridge."""
|
2021-11-16 19:59:17 +00:00
|
|
|
return self.config_entry.data[CONF_HOST]
|
2020-07-02 12:12:24 +00:00
|
|
|
|
|
|
|
@property
|
2021-11-16 19:59:17 +00:00
|
|
|
def api_version(self) -> int:
|
|
|
|
"""Return api version we're set-up for."""
|
|
|
|
return self.config_entry.data[CONF_API_VERSION]
|
2019-12-16 18:45:09 +00:00
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
async def async_initialize_bridge(self) -> bool:
|
|
|
|
"""Initialize Connection with the Hue API."""
|
2018-03-30 03:15:40 +00:00
|
|
|
try:
|
2021-11-16 19:59:17 +00:00
|
|
|
with async_timeout.timeout(10):
|
|
|
|
await self.api.initialize()
|
2019-12-16 18:45:09 +00:00
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
except (LinkButtonNotPressed, Unauthorized):
|
2019-02-14 15:01:46 +00:00
|
|
|
# Usernames can become invalid if hub is reset or user removed.
|
2018-03-30 03:15:40 +00:00
|
|
|
# We are going to fail the config entry setup and initiate a new
|
|
|
|
# linking procedure. When linking succeeds, it will remove the
|
|
|
|
# old config entry.
|
2021-11-16 19:59:17 +00:00
|
|
|
create_config_flow(self.hass, self.host)
|
2018-03-30 03:15:40 +00:00
|
|
|
return False
|
2021-11-16 19:59:17 +00:00
|
|
|
except (
|
|
|
|
asyncio.TimeoutError,
|
|
|
|
client_exceptions.ClientOSError,
|
|
|
|
client_exceptions.ServerDisconnectedError,
|
|
|
|
client_exceptions.ContentTypeError,
|
2021-12-23 13:04:10 +00:00
|
|
|
BridgeBusy,
|
2021-11-16 19:59:17 +00:00
|
|
|
) as err:
|
2021-04-26 02:42:45 +00:00
|
|
|
raise ConfigEntryNotReady(
|
2021-11-16 19:59:17 +00:00
|
|
|
f"Error connecting to the Hue bridge at {self.host}"
|
2021-04-26 02:42:45 +00:00
|
|
|
) from err
|
2018-03-30 03:15:40 +00:00
|
|
|
except Exception: # pylint: disable=broad-except
|
2021-11-16 19:59:17 +00:00
|
|
|
self.logger.exception("Unknown error connecting to Hue bridge")
|
2018-03-30 03:15:40 +00:00
|
|
|
return False
|
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
# v1 specific initialization/setup code here
|
|
|
|
if self.api_version == 1:
|
|
|
|
if self.api.sensors is not None:
|
|
|
|
self.sensor_manager = SensorManager(self)
|
|
|
|
self.hass.config_entries.async_setup_platforms(
|
|
|
|
self.config_entry, PLATFORMS_v1
|
|
|
|
)
|
|
|
|
|
|
|
|
# v2 specific initialization/setup code here
|
|
|
|
else:
|
|
|
|
await async_setup_devices(self)
|
|
|
|
await async_setup_hue_events(self)
|
|
|
|
self.hass.config_entries.async_setup_platforms(
|
|
|
|
self.config_entry, PLATFORMS_v2
|
|
|
|
)
|
|
|
|
|
|
|
|
# add listener for config entry updates.
|
2021-05-14 20:39:57 +00:00
|
|
|
self.reset_jobs.append(self.config_entry.add_update_listener(_update_listener))
|
2019-10-28 15:45:08 +00:00
|
|
|
self.authorized = True
|
2018-03-30 03:15:40 +00:00
|
|
|
return True
|
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
async def async_request_call(
|
|
|
|
self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs
|
|
|
|
) -> Any:
|
2021-12-23 13:04:10 +00:00
|
|
|
"""Send request to the Hue bridge, optionally omitting error(s)."""
|
|
|
|
try:
|
|
|
|
return await task(*args, **kwargs)
|
|
|
|
except AiohueException as err:
|
|
|
|
# The (new) Hue api can be a bit fanatic with throwing errors
|
|
|
|
# some of which we accept in certain conditions
|
|
|
|
# handle that here. Note that these errors are strings and do not have
|
|
|
|
# an identifier or something.
|
|
|
|
if allowed_errors is not None and str(err) in allowed_errors:
|
|
|
|
# log only
|
|
|
|
self.logger.debug("Ignored error/warning from Hue API: %s", str(err))
|
|
|
|
return None
|
|
|
|
raise err
|
2019-12-01 21:24:16 +00:00
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
async def async_reset(self) -> bool:
|
2018-04-12 12:28:54 +00:00
|
|
|
"""Reset this bridge to default state.
|
|
|
|
|
|
|
|
Will cancel any scheduled setup retry and will unload
|
|
|
|
the config entry.
|
|
|
|
"""
|
|
|
|
# The bridge can be in 3 states:
|
|
|
|
# - Setup was successful, self.api is not None
|
|
|
|
# - Authentication was wrong, self.api is None, not retrying setup.
|
|
|
|
|
|
|
|
# If the authentication was wrong.
|
|
|
|
if self.api is None:
|
|
|
|
return True
|
|
|
|
|
2020-01-31 22:47:40 +00:00
|
|
|
while self.reset_jobs:
|
|
|
|
self.reset_jobs.pop()()
|
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
# Unload platforms
|
2021-05-20 07:08:23 +00:00
|
|
|
unload_success = await self.hass.config_entries.async_unload_platforms(
|
2021-11-16 19:59:17 +00:00
|
|
|
self.config_entry, PLATFORMS_v1 if self.api_version == 1 else PLATFORMS_v2
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2020-01-31 22:47:40 +00:00
|
|
|
|
2021-05-20 07:08:23 +00:00
|
|
|
if unload_success:
|
|
|
|
self.hass.data[DOMAIN].pop(self.config_entry.entry_id)
|
|
|
|
|
|
|
|
return unload_success
|
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
async def handle_unauthorized_error(self) -> None:
|
2019-10-28 15:45:08 +00:00
|
|
|
"""Create a new config flow when the authorization is no longer valid."""
|
|
|
|
if not self.authorized:
|
|
|
|
# we already created a new config flow, no need to do it again
|
|
|
|
return
|
2021-11-16 19:59:17 +00:00
|
|
|
self.logger.error(
|
2021-03-19 14:26:36 +00:00
|
|
|
"Unable to authorize to bridge %s, setup the linking again", self.host
|
2019-10-28 15:45:08 +00:00
|
|
|
)
|
|
|
|
self.authorized = False
|
|
|
|
create_config_flow(self.hass, self.host)
|
|
|
|
|
2021-11-16 19:59:17 +00:00
|
|
|
|
|
|
|
async def _update_listener(hass: core.HomeAssistant, entry: ConfigEntry) -> None:
|
|
|
|
"""Handle ConfigEntry options update."""
|
2020-07-02 12:12:24 +00:00
|
|
|
await hass.config_entries.async_reload(entry.entry_id)
|
2021-11-16 19:59:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
def create_config_flow(hass: core.HomeAssistant, host: str) -> None:
|
|
|
|
"""Start a config flow."""
|
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.flow.async_init(
|
|
|
|
DOMAIN,
|
|
|
|
context={"source": SOURCE_IMPORT},
|
|
|
|
data={"host": host},
|
|
|
|
)
|
|
|
|
)
|