Home Assistant Unofficial Reference 2024.12.1
hap.py
Go to the documentation of this file.
1 """Access point for the HomematicIP Cloud component."""
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 from homematicip.aio.auth import AsyncAuth
11 from homematicip.aio.home import AsyncHome
12 from homematicip.base.base_connection import HmipConnectionError
13 from homematicip.base.enums import EventType
14 
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.core import HomeAssistant, callback
17 from homeassistant.exceptions import ConfigEntryNotReady
18 from homeassistant.helpers.aiohttp_client import async_get_clientsession
19 
20 from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN, PLATFORMS
21 from .errors import HmipcConnectionError
22 
23 _LOGGER = logging.getLogger(__name__)
24 
25 
27  """Manages HomematicIP client registration."""
28 
29  auth: AsyncAuth
30 
31  def __init__(self, hass: HomeAssistant, config: dict[str, str]) -> None:
32  """Initialize HomematicIP Cloud client registration."""
33  self.hasshass = hass
34  self.configconfig = config
35 
36  async def async_setup(self) -> bool:
37  """Connect to HomematicIP for registration."""
38  try:
39  self.authauth = await self.get_authget_auth(
40  self.hasshass, self.configconfig.get(HMIPC_HAPID), self.configconfig.get(HMIPC_PIN)
41  )
42  except HmipcConnectionError:
43  return False
44  return self.authauth is not None
45 
46  async def async_checkbutton(self) -> bool:
47  """Check blue butten has been pressed."""
48  try:
49  return await self.authauth.isRequestAcknowledged()
50  except HmipConnectionError:
51  return False
52 
53  async def async_register(self):
54  """Register client at HomematicIP."""
55  try:
56  authtoken = await self.authauth.requestAuthToken()
57  await self.authauth.confirmAuthToken(authtoken)
58  except HmipConnectionError:
59  return False
60  return authtoken
61 
62  async def get_auth(self, hass: HomeAssistant, hapid, pin):
63  """Create a HomematicIP access point object."""
64  auth = AsyncAuth(hass.loop, async_get_clientsession(hass))
65  try:
66  await auth.init(hapid)
67  if pin:
68  auth.pin = pin
69  await auth.connectionRequest("HomeAssistant")
70  except HmipConnectionError:
71  return None
72  return auth
73 
74 
76  """Manages HomematicIP HTTP and WebSocket connection."""
77 
78  home: AsyncHome
79 
80  def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
81  """Initialize HomematicIP Cloud connection."""
82  self.hasshass = hass
83  self.config_entryconfig_entry = config_entry
84 
85  self._ws_close_requested_ws_close_requested = False
86  self._retry_task_retry_task: asyncio.Task | None = None
87  self._tries_tries = 0
88  self._accesspoint_connected_accesspoint_connected = True
89  self.hmip_device_by_entity_idhmip_device_by_entity_id: dict[str, Any] = {}
90  self.reset_connection_listener: Callable | None = None
91 
92  async def async_setup(self, tries: int = 0) -> bool:
93  """Initialize connection."""
94  try:
95  self.homehome = await self.get_hapget_hap(
96  self.hasshass,
97  self.config_entryconfig_entry.data.get(HMIPC_HAPID),
98  self.config_entryconfig_entry.data.get(HMIPC_AUTHTOKEN),
99  self.config_entryconfig_entry.data.get(HMIPC_NAME),
100  )
101  except HmipcConnectionError as err:
102  raise ConfigEntryNotReady from err
103  except Exception as err: # noqa: BLE001
104  _LOGGER.error("Error connecting with HomematicIP Cloud: %s", err)
105  return False
106 
107  _LOGGER.debug(
108  "Connected to HomematicIP with HAP %s", self.config_entryconfig_entry.unique_id
109  )
110 
111  await self.hasshass.config_entries.async_forward_entry_setups(
112  self.config_entryconfig_entry, PLATFORMS
113  )
114 
115  return True
116 
117  @callback
118  def async_update(self, *args, **kwargs) -> None:
119  """Async update the home device.
120 
121  Triggered when the HMIP HOME_CHANGED event has fired.
122  There are several occasions for this event to happen.
123  1. We are interested to check whether the access point
124  is still connected. If not, entity state changes cannot
125  be forwarded to hass. So if access point is disconnected all devices
126  are set to unavailable.
127  2. We need to update home including devices and groups after a reconnect.
128  3. We need to update home without devices and groups in all other cases.
129 
130  """
131  if not self.homehome.connected:
132  _LOGGER.error("HMIP access point has lost connection with the cloud")
133  self._accesspoint_connected_accesspoint_connected = False
134  self.set_all_to_unavailableset_all_to_unavailable()
135  elif not self._accesspoint_connected_accesspoint_connected:
136  # Now the HOME_CHANGED event has fired indicating the access
137  # point has reconnected to the cloud again.
138  # Explicitly getting an update as entity states might have
139  # changed during access point disconnect."""
140 
141  job = self.hasshass.async_create_task(self.get_stateget_state())
142  job.add_done_callback(self.get_state_finishedget_state_finished)
143  self._accesspoint_connected_accesspoint_connected = True
144 
145  @callback
146  def async_create_entity(self, *args, **kwargs) -> None:
147  """Create an entity or a group."""
148  is_device = EventType(kwargs["event_type"]) == EventType.DEVICE_ADDED
149  self.hasshass.async_create_task(self.async_create_entity_lazyasync_create_entity_lazy(is_device))
150 
151  async def async_create_entity_lazy(self, is_device=True) -> None:
152  """Delay entity creation to allow the user to enter a device name."""
153  if is_device:
154  await asyncio.sleep(30)
155  await self.hasshass.config_entries.async_reload(self.config_entryconfig_entry.entry_id)
156 
157  async def get_state(self) -> None:
158  """Update HMIP state and tell Home Assistant."""
159  await self.homehome.get_current_state()
160  self.update_allupdate_all()
161 
162  def get_state_finished(self, future) -> None:
163  """Execute when get_state coroutine has finished."""
164  try:
165  future.result()
166  except HmipConnectionError:
167  # Somehow connection could not recover. Will disconnect and
168  # so reconnect loop is taking over.
169  _LOGGER.error("Updating state after HMIP access point reconnect failed")
170  self.hasshass.async_create_task(self.homehome.disable_events())
171 
172  def set_all_to_unavailable(self) -> None:
173  """Set all devices to unavailable and tell Home Assistant."""
174  for device in self.homehome.devices:
175  device.unreach = True
176  self.update_allupdate_all()
177 
178  def update_all(self) -> None:
179  """Signal all devices to update their state."""
180  for device in self.homehome.devices:
181  device.fire_update_event()
182 
183  async def async_connect(self) -> None:
184  """Start WebSocket connection."""
185  tries = 0
186  while True:
187  retry_delay = 2 ** min(tries, 8)
188 
189  try:
190  await self.homehome.get_current_state()
191  hmip_events = await self.homehome.enable_events()
192  tries = 0
193  await hmip_events
194  except HmipConnectionError:
195  _LOGGER.error(
196  (
197  "Error connecting to HomematicIP with HAP %s. "
198  "Retrying in %d seconds"
199  ),
200  self.config_entryconfig_entry.unique_id,
201  retry_delay,
202  )
203 
204  if self._ws_close_requested_ws_close_requested:
205  break
206  self._ws_close_requested_ws_close_requested = False
207  tries += 1
208 
209  try:
210  self._retry_task_retry_task = self.hasshass.async_create_task(
211  asyncio.sleep(retry_delay)
212  )
213  await self._retry_task_retry_task
214  except asyncio.CancelledError:
215  break
216 
217  async def async_reset(self) -> bool:
218  """Close the websocket connection."""
219  self._ws_close_requested_ws_close_requested = True
220  if self._retry_task_retry_task is not None:
221  self._retry_task_retry_task.cancel()
222  await self.homehome.disable_events()
223  _LOGGER.debug("Closed connection to HomematicIP cloud server")
224  await self.hasshass.config_entries.async_unload_platforms(
225  self.config_entryconfig_entry, PLATFORMS
226  )
227  self.hmip_device_by_entity_idhmip_device_by_entity_id = {}
228  return True
229 
230  @callback
231  def shutdown(self, event) -> None:
232  """Wrap the call to async_reset.
233 
234  Used as an argument to EventBus.async_listen_once.
235  """
236  self.hasshass.async_create_task(self.async_resetasync_reset())
237  _LOGGER.debug(
238  "Reset connection to access point id %s", self.config_entryconfig_entry.unique_id
239  )
240 
241  async def get_hap(
242  self,
243  hass: HomeAssistant,
244  hapid: str | None,
245  authtoken: str | None,
246  name: str | None,
247  ) -> AsyncHome:
248  """Create a HomematicIP access point object."""
249  home = AsyncHome(hass.loop, async_get_clientsession(hass))
250 
251  home.name = name
252  # Use the title of the config entry as title for the home.
253  home.label = self.config_entryconfig_entry.title
254  home.modelType = "HomematicIP Cloud Home"
255 
256  home.set_auth_token(authtoken)
257  try:
258  await home.init(hapid)
259  await home.get_current_state()
260  except HmipConnectionError as err:
261  raise HmipcConnectionError from err
262  home.on_update(self.async_updateasync_update)
263  home.on_create(self.async_create_entityasync_create_entity)
264  hass.loop.create_task(self.async_connectasync_connect())
265 
266  return home
def get_auth(self, HomeAssistant hass, hapid, pin)
Definition: hap.py:62
None __init__(self, HomeAssistant hass, dict[str, str] config)
Definition: hap.py:31
AsyncHome get_hap(self, HomeAssistant hass, str|None hapid, str|None authtoken, str|None name)
Definition: hap.py:247
None __init__(self, HomeAssistant hass, ConfigEntry config_entry)
Definition: hap.py:80
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)