Home Assistant Unofficial Reference 2024.12.1
bridge.py
Go to the documentation of this file.
1 """Code to handle a Hue bridge."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable
7 import logging
8 from typing import Any
9 
10 import aiohttp
11 from aiohttp import client_exceptions
12 from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized
13 from aiohue.errors import AiohueException, BridgeBusy
14 
15 from homeassistant import core
16 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
17 from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST, Platform
18 from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
19 from homeassistant.helpers import aiohttp_client
20 
21 from .const import DOMAIN
22 from .v1.sensor_base import SensorManager
23 from .v2.device import async_setup_devices
24 from .v2.hue_event import async_setup_hue_events
25 
26 # How long should we sleep if the hub is busy
27 HUB_BUSY_SLEEP = 0.5
28 
29 PLATFORMS_v1 = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR]
30 PLATFORMS_v2 = [
31  Platform.BINARY_SENSOR,
32  Platform.EVENT,
33  Platform.LIGHT,
34  Platform.SCENE,
35  Platform.SENSOR,
36  Platform.SWITCH,
37 ]
38 
39 
40 class HueBridge:
41  """Manages a single Hue bridge."""
42 
43  def __init__(self, hass: core.HomeAssistant, config_entry: ConfigEntry) -> None:
44  """Initialize the system."""
45  self.config_entryconfig_entry = config_entry
46  self.hasshass = hass
47  self.authorizedauthorized = False
48  # Jobs to be executed when API is reset.
49  self.reset_jobs: list[core.CALLBACK_TYPE] = []
50  self.sensor_managersensor_manager: SensorManager | None = None
51  self.loggerlogger = logging.getLogger(__name__)
52  # store actual api connection to bridge as api
53  app_key: str = self.config_entryconfig_entry.data[CONF_API_KEY]
54  if self.api_versionapi_versionapi_version == 1:
55  self.apiapi = HueBridgeV1(
56  self.hosthost, app_key, aiohttp_client.async_get_clientsession(hass)
57  )
58  else:
59  self.apiapi = HueBridgeV2(self.hosthost, app_key)
60  # store (this) bridge object in hass data
61  hass.data.setdefault(DOMAIN, {})[self.config_entryconfig_entry.entry_id] = self
62 
63  @property
64  def host(self) -> str:
65  """Return the host of this bridge."""
66  return self.config_entryconfig_entry.data[CONF_HOST]
67 
68  @property
69  def api_version(self) -> int:
70  """Return api version we're set-up for."""
71  return self.config_entryconfig_entry.data[CONF_API_VERSION]
72 
73  async def async_initialize_bridge(self) -> bool:
74  """Initialize Connection with the Hue API."""
75  setup_ok = False
76  try:
77  async with asyncio.timeout(10):
78  await self.apiapi.initialize()
79  setup_ok = True
80  except (LinkButtonNotPressed, Unauthorized):
81  # Usernames can become invalid if hub is reset or user removed.
82  # We are going to fail the config entry setup and initiate a new
83  # linking procedure. When linking succeeds, it will remove the
84  # old config entry.
85  create_config_flow(self.hasshass, self.hosthost)
86  return False
87  except (
88  TimeoutError,
89  client_exceptions.ClientOSError,
90  client_exceptions.ServerDisconnectedError,
91  client_exceptions.ContentTypeError,
92  BridgeBusy,
93  ) as err:
94  raise ConfigEntryNotReady(
95  f"Error connecting to the Hue bridge at {self.host}"
96  ) from err
97  except Exception:
98  self.loggerlogger.exception("Unknown error connecting to Hue bridge")
99  return False
100  finally:
101  if not setup_ok:
102  await self.apiapi.close()
103 
104  # v1 specific initialization/setup code here
105  if self.api_versionapi_versionapi_version == 1:
106  if self.apiapi.sensors is not None:
107  self.sensor_managersensor_manager = SensorManager(self)
108  await self.hasshass.config_entries.async_forward_entry_setups(
109  self.config_entryconfig_entry, PLATFORMS_v1
110  )
111 
112  # v2 specific initialization/setup code here
113  else:
114  await async_setup_devices(self)
115  await async_setup_hue_events(self)
116  await self.hasshass.config_entries.async_forward_entry_setups(
117  self.config_entryconfig_entry, PLATFORMS_v2
118  )
119 
120  # add listener for config entry updates.
121  self.reset_jobs.append(self.config_entryconfig_entry.add_update_listener(_update_listener))
122  self.authorizedauthorized = True
123  return True
124 
125  async def async_request_call(self, task: Callable, *args, **kwargs) -> Any:
126  """Send request to the Hue bridge."""
127  try:
128  return await task(*args, **kwargs)
129  except AiohueException as err:
130  # The (new) Hue api can be a bit fanatic with throwing errors so
131  # we have some logic to treat some responses as warning only.
132  msg = f"Request failed: {err}"
133  if "may not have effect" in str(err):
134  # log only
135  self.loggerlogger.debug(msg)
136  return None
137  raise HomeAssistantError(msg) from err
138  except aiohttp.ClientError as err:
139  raise HomeAssistantError(
140  f"Request failed due connection error: {err}"
141  ) from err
142 
143  async def async_reset(self) -> bool:
144  """Reset this bridge to default state.
145 
146  Will cancel any scheduled setup retry and will unload
147  the config entry.
148  """
149  # The bridge can be in 3 states:
150  # - Setup was successful, self.api is not None
151  # - Authentication was wrong, self.api is None, not retrying setup.
152 
153  # If the authentication was wrong.
154  if self.apiapi is None:
155  return True
156 
157  while self.reset_jobs:
158  self.reset_jobs.pop()()
159 
160  # Unload platforms
161  unload_success = await self.hasshass.config_entries.async_unload_platforms(
162  self.config_entryconfig_entry, PLATFORMS_v1 if self.api_versionapi_versionapi_version == 1 else PLATFORMS_v2
163  )
164 
165  if unload_success:
166  self.hasshass.data[DOMAIN].pop(self.config_entryconfig_entry.entry_id)
167 
168  return unload_success
169 
170  async def handle_unauthorized_error(self) -> None:
171  """Create a new config flow when the authorization is no longer valid."""
172  if not self.authorizedauthorized:
173  # we already created a new config flow, no need to do it again
174  return
175  self.loggerlogger.error(
176  "Unable to authorize to bridge %s, setup the linking again", self.hosthost
177  )
178  self.authorizedauthorized = False
179  create_config_flow(self.hasshass, self.hosthost)
180 
181 
182 async def _update_listener(hass: core.HomeAssistant, entry: ConfigEntry) -> None:
183  """Handle ConfigEntry options update."""
184  await hass.config_entries.async_reload(entry.entry_id)
185 
186 
187 def create_config_flow(hass: core.HomeAssistant, host: str) -> None:
188  """Start a config flow."""
189  hass.async_create_task(
190  hass.config_entries.flow.async_init(
191  DOMAIN,
192  context={"source": SOURCE_IMPORT},
193  data={"host": host},
194  )
195  )
Any async_request_call(self, Callable task, *args, **kwargs)
Definition: bridge.py:125
None __init__(self, core.HomeAssistant hass, ConfigEntry config_entry)
Definition: bridge.py:43
None create_config_flow(core.HomeAssistant hass, str host)
Definition: bridge.py:187
None _update_listener(core.HomeAssistant hass, ConfigEntry entry)
Definition: bridge.py:182
def async_setup_devices(HueBridge bridge)
Definition: device.py:32
def async_setup_hue_events(HueBridge bridge)
Definition: hue_event.py:30