Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Minut Point."""
2 
3 import asyncio
4 from dataclasses import dataclass
5 from http import HTTPStatus
6 import logging
7 
8 from aiohttp import ClientError, ClientResponseError, web
9 from pypoint import PointSession
10 import voluptuous as vol
11 
12 from homeassistant.components import webhook
14  ClientCredential,
15  async_import_client_credential,
16 )
17 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
18 from homeassistant.const import (
19  CONF_CLIENT_ID,
20  CONF_CLIENT_SECRET,
21  CONF_WEBHOOK_ID,
22  Platform,
23 )
24 from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
25 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
26 from homeassistant.helpers import (
27  aiohttp_client,
28  config_entry_oauth2_flow,
29  config_validation as cv,
30 )
31 from homeassistant.helpers.dispatcher import async_dispatcher_send
32 from homeassistant.helpers.event import async_track_time_interval
33 from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
34 from homeassistant.helpers.typing import ConfigType
35 
36 from . import api
37 from .const import (
38  CONF_WEBHOOK_URL,
39  DOMAIN,
40  EVENT_RECEIVED,
41  POINT_DISCOVERY_NEW,
42  SCAN_INTERVAL,
43  SIGNAL_UPDATE_ENTITY,
44  SIGNAL_WEBHOOK,
45 )
46 
47 _LOGGER = logging.getLogger(__name__)
48 
49 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
50 
51 type PointConfigEntry = ConfigEntry[PointData]
52 
53 CONFIG_SCHEMA = vol.Schema(
54  {
55  DOMAIN: vol.Schema(
56  {
57  vol.Required(CONF_CLIENT_ID): cv.string,
58  vol.Required(CONF_CLIENT_SECRET): cv.string,
59  }
60  )
61  },
62  extra=vol.ALLOW_EXTRA,
63 )
64 
65 
66 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
67  """Set up the Minut Point component."""
68  if DOMAIN not in config:
69  return True
70 
71  conf = config[DOMAIN]
72 
74  hass,
75  HOMEASSISTANT_DOMAIN,
76  f"deprecated_yaml_{DOMAIN}",
77  breaks_in_ha_version="2025.4.0",
78  is_fixable=False,
79  issue_domain=DOMAIN,
80  severity=IssueSeverity.WARNING,
81  translation_key="deprecated_yaml",
82  translation_placeholders={
83  "domain": DOMAIN,
84  "integration_title": "Point",
85  },
86  )
87 
88  if not hass.config_entries.async_entries(DOMAIN):
90  hass,
91  DOMAIN,
93  conf[CONF_CLIENT_ID],
94  conf[CONF_CLIENT_SECRET],
95  ),
96  )
97 
98  hass.async_create_task(
99  hass.config_entries.flow.async_init(
100  DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
101  )
102  )
103 
104  return True
105 
106 
107 async def async_setup_entry(hass: HomeAssistant, entry: PointConfigEntry) -> bool:
108  """Set up Minut Point from a config entry."""
109 
110  if "auth_implementation" not in entry.data:
111  raise ConfigEntryAuthFailed("Authentication failed. Please re-authenticate.")
112 
113  implementation = (
114  await config_entry_oauth2_flow.async_get_config_entry_implementation(
115  hass, entry
116  )
117  )
118  session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
120  aiohttp_client.async_get_clientsession(hass), session
121  )
122 
123  try:
124  await auth.async_get_access_token()
125  except ClientResponseError as err:
126  if err.status in {HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN}:
127  raise ConfigEntryAuthFailed from err
128  raise ConfigEntryNotReady from err
129  except ClientError as err:
130  raise ConfigEntryNotReady from err
131 
132  point_session = PointSession(auth)
133 
134  client = MinutPointClient(hass, entry, point_session)
135  hass.async_create_task(client.update())
136  entry.runtime_data = PointData(client)
137 
138  await async_setup_webhook(hass, entry, point_session)
139  await hass.config_entries.async_forward_entry_setups(
140  entry, [*PLATFORMS, Platform.ALARM_CONTROL_PANEL]
141  )
142 
143  return True
144 
145 
147  hass: HomeAssistant, entry: PointConfigEntry, session: PointSession
148 ) -> None:
149  """Set up a webhook to handle binary sensor events."""
150  if CONF_WEBHOOK_ID not in entry.data:
151  webhook_id = webhook.async_generate_id()
152  webhook_url = webhook.async_generate_url(hass, webhook_id)
153  _LOGGER.debug("Registering new webhook at: %s", webhook_url)
154 
155  hass.config_entries.async_update_entry(
156  entry,
157  data={
158  **entry.data,
159  CONF_WEBHOOK_ID: webhook_id,
160  CONF_WEBHOOK_URL: webhook_url,
161  },
162  )
163 
164  await session.update_webhook(
165  webhook.async_generate_url(hass, entry.data[CONF_WEBHOOK_ID]),
166  entry.data[CONF_WEBHOOK_ID],
167  ["*"],
168  )
169  webhook.async_register(
170  hass, DOMAIN, "Point", entry.data[CONF_WEBHOOK_ID], handle_webhook
171  )
172 
173 
174 async def async_unload_entry(hass: HomeAssistant, entry: PointConfigEntry) -> bool:
175  """Unload a config entry."""
176  if unload_ok := await hass.config_entries.async_unload_platforms(
177  entry, [*PLATFORMS, Platform.ALARM_CONTROL_PANEL]
178  ):
179  session: PointSession = entry.runtime_data.client
180  if CONF_WEBHOOK_ID in entry.data:
181  webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
182  await session.remove_webhook()
183  return unload_ok
184 
185 
186 async def handle_webhook(
187  hass: HomeAssistant, webhook_id: str, request: web.Request
188 ) -> None:
189  """Handle webhook callback."""
190  try:
191  data = await request.json()
192  _LOGGER.debug("Webhook %s: %s", webhook_id, data)
193  except ValueError:
194  return
195 
196  if isinstance(data, dict):
197  data["webhook_id"] = webhook_id
198  async_dispatcher_send(hass, SIGNAL_WEBHOOK, data, data.get("hook_id"))
199  hass.bus.async_fire(EVENT_RECEIVED, data)
200 
201 
203  """Get the latest data and update the states."""
204 
205  def __init__(
206  self, hass: HomeAssistant, config_entry: ConfigEntry, session: PointSession
207  ) -> None:
208  """Initialize the Minut data object."""
209  self._known_devices: set[str] = set()
210  self._known_homes: set[str] = set()
211  self._hass_hass = hass
212  self._config_entry_config_entry = config_entry
213  self._is_available_is_available = True
214  self._client_client = session
215 
216  async_track_time_interval(self._hass_hass, self.updateupdate, SCAN_INTERVAL)
217 
218  async def update(self, *args):
219  """Periodically poll the cloud for current state."""
220  await self._sync_sync()
221 
222  async def _sync(self):
223  """Update local list of devices."""
224  if not await self._client_client.update():
225  self._is_available_is_available = False
226  _LOGGER.warning("Device is unavailable")
227  async_dispatcher_send(self._hass_hass, SIGNAL_UPDATE_ENTITY)
228  return
229 
230  self._is_available_is_available = True
231  for home_id in self._client_client.homes:
232  if home_id not in self._known_homes:
234  self._hass_hass,
235  POINT_DISCOVERY_NEW.format(Platform.ALARM_CONTROL_PANEL),
236  home_id,
237  )
238  self._known_homes.add(home_id)
239  for device in self._client_client.devices:
240  if device.device_id not in self._known_devices:
241  for platform in PLATFORMS:
243  self._hass_hass,
244  POINT_DISCOVERY_NEW.format(platform),
245  device.device_id,
246  )
247  self._known_devices.add(device.device_id)
248  async_dispatcher_send(self._hass_hass, SIGNAL_UPDATE_ENTITY)
249 
250  def device(self, device_id):
251  """Return device representation."""
252  return self._client_client.device(device_id)
253 
254  def is_available(self, device_id):
255  """Return device availability."""
256  if not self._is_available_is_available:
257  return False
258  return device_id in self._client_client.device_ids
259 
260  async def remove_webhook(self):
261  """Remove the session webhook."""
262  return await self._client_client.remove_webhook()
263 
264  @property
265  def homes(self):
266  """Return known homes."""
267  return self._client_client.homes
268 
269  async def async_alarm_disarm(self, home_id):
270  """Send alarm disarm command."""
271  return await self._client_client.alarm_disarm(home_id)
272 
273  async def async_alarm_arm(self, home_id):
274  """Send alarm arm command."""
275  return await self._client_client.alarm_arm(home_id)
276 
277 
278 @dataclass
279 class PointData:
280  """Point Data."""
281 
282  client: MinutPointClient
283  entry_lock: asyncio.Lock = asyncio.Lock()
None __init__(self, HomeAssistant hass, ConfigEntry config_entry, PointSession session)
Definition: __init__.py:207
None async_import_client_credential(HomeAssistant hass, str domain, ClientCredential credential, str|None auth_domain=None)
Definition: __init__.py:175
bool add(self, _T matcher)
Definition: match.py:185
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:66
None handle_webhook(HomeAssistant hass, str webhook_id, web.Request request)
Definition: __init__.py:188
bool async_unload_entry(HomeAssistant hass, PointConfigEntry entry)
Definition: __init__.py:174
bool async_setup_entry(HomeAssistant hass, PointConfigEntry entry)
Definition: __init__.py:107
None async_setup_webhook(HomeAssistant hass, PointConfigEntry entry, PointSession session)
Definition: __init__.py:148
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679