1 """The nuki component."""
3 from __future__
import annotations
6 from dataclasses
import dataclass
7 from http
import HTTPStatus
10 from aiohttp
import web
11 from pynuki
import NukiBridge, NukiLock, NukiOpener
12 from pynuki.bridge
import InvalidCredentialsException
13 from requests.exceptions
import RequestException
15 from homeassistant
import exceptions
22 EVENT_HOMEASSISTANT_STOP,
30 from .const
import CONF_ENCRYPT_TOKEN, DEFAULT_TIMEOUT, DOMAIN
31 from .coordinator
import NukiCoordinator
32 from .helpers
import NukiWebhookException, parse_id
34 _LOGGER = logging.getLogger(__name__)
36 PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR]
39 @dataclass(slots=True)
41 """Class to hold Nuki data."""
43 coordinator: NukiCoordinator
46 openers: list[NukiOpener]
50 return bridge.locks, bridge.openers
54 hass: HomeAssistant, entry: ConfigEntry, bridge: NukiBridge
58 hass: HomeAssistant, webhook_id: str, request: web.Request
60 """Handle webhook callback."""
62 data = await request.json()
64 return web.Response(status=HTTPStatus.BAD_REQUEST)
66 entry_data: NukiEntryData = hass.data[DOMAIN][entry.entry_id]
67 locks = entry_data.locks
68 openers = entry_data.openers
70 devices = [x
for x
in locks + openers
if x.nuki_id == data[
"nukiId"]]
72 devices[0].update_from_callback(data)
74 coordinator = entry_data.coordinator
75 coordinator.async_set_updated_data(
None)
77 return web.Response(status=HTTPStatus.OK)
79 webhook.async_register(
80 hass, DOMAIN, entry.title, entry.entry_id, handle_webhook, local_only=
True
83 webhook_url = webhook.async_generate_path(entry.entry_id)
93 except NoURLAvailableError:
94 webhook.async_unregister(hass, entry.entry_id)
96 f
"Error registering URL for webhook {entry.entry_id}: "
97 "HomeAssistant URL is not available"
100 url = f
"{hass_url}{webhook_url}"
102 if hass_url.startswith(
"https"):
103 ir.async_create_issue(
108 severity=ir.IssueSeverity.WARNING,
109 translation_key=
"https_webhook",
110 translation_placeholders={
111 "base_url": hass_url,
112 "network_link":
"https://my.home-assistant.io/redirect/network/",
116 ir.async_delete_issue(hass, DOMAIN,
"https_webhook")
119 async
with asyncio.timeout(10):
120 await hass.async_add_executor_job(
121 _register_webhook, bridge, entry.entry_id, url
123 except InvalidCredentialsException
as err:
124 webhook.async_unregister(hass, entry.entry_id)
126 f
"Invalid credentials for Bridge: {err}"
128 except RequestException
as err:
129 webhook.async_unregister(hass, entry.entry_id)
131 f
"Error communicating with Bridge: {err}"
137 callbacks = bridge.callback_list()
138 for item
in callbacks[
"callbacks"]:
139 if entry_id
in item[
"url"]:
140 if item[
"url"] == url:
142 bridge.callback_remove(item[
"id"])
144 if bridge.callback_add(url)[
"success"]:
152 callbacks = bridge.callback_list()
153 for item
in callbacks[
"callbacks"]:
154 if entry_id
in item[
"url"]:
155 bridge.callback_remove(item[
"id"])
159 """Set up the Nuki entry."""
161 hass.data.setdefault(DOMAIN, {})
164 if isinstance(entry.unique_id, int):
166 params = {
"unique_id": new_id}
167 if entry.title == entry.unique_id:
168 params[
"title"] = new_id
169 hass.config_entries.async_update_entry(entry, **params)
172 bridge = await hass.async_add_executor_job(
174 entry.data[CONF_HOST],
175 entry.data[CONF_TOKEN],
176 entry.data[CONF_PORT],
177 entry.data.get(CONF_ENCRYPT_TOKEN,
True),
181 locks, openers = await hass.async_add_executor_job(_get_bridge_devices, bridge)
182 except InvalidCredentialsException
as err:
184 except RequestException
as err:
189 bridge_id =
parse_id(info[
"ids"][
"hardwareId"])
190 dev_reg = dr.async_get(hass)
191 dev_reg.async_get_or_create(
192 config_entry_id=entry.entry_id,
193 identifiers={(DOMAIN, bridge_id)},
194 manufacturer=
"Nuki Home Solutions GmbH",
195 name=f
"Nuki Bridge {bridge_id}",
196 model=
"Hardware Bridge",
197 sw_version=info[
"versions"][
"firmwareVersion"],
198 serial_number=
parse_id(info[
"ids"][
"hardwareId"]),
203 except NukiWebhookException
as err:
204 _LOGGER.warning(
"Error registering HomeAssistant webhook: %s", err)
206 async
def _stop_nuki(_: Event):
207 """Stop and remove the Nuki webhook."""
208 webhook.async_unregister(hass, entry.entry_id)
210 async
with asyncio.timeout(10):
211 await hass.async_add_executor_job(
212 _remove_webhook, bridge, entry.entry_id
214 except InvalidCredentialsException
as err:
216 "Error unregistering webhook, invalid credentials for bridge: %s", err
218 except RequestException
as err:
219 _LOGGER.error(
"Error communicating with bridge: %s", err)
221 entry.async_on_unload(
222 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_nuki)
227 coordinator=coordinator,
234 await coordinator.async_refresh()
236 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
242 """Unload the Nuki entry."""
243 webhook.async_unregister(hass, entry.entry_id)
244 entry_data: NukiEntryData = hass.data[DOMAIN][entry.entry_id]
247 async
with asyncio.timeout(10):
248 await hass.async_add_executor_job(
253 except InvalidCredentialsException
as err:
255 f
"Unable to remove callback. Invalid credentials for Bridge: {err}"
257 except RequestException
as err:
259 f
"Unable to remove callback. Error communicating with Bridge: {err}"
262 unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
264 hass.data[DOMAIN].pop(entry.entry_id)
def parse_id(hardware_id)
tuple[list[NukiLock], list[NukiOpener]] _get_bridge_devices(NukiBridge bridge)
None _remove_webhook(NukiBridge bridge, str entry_id)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
None _create_webhook(HomeAssistant hass, ConfigEntry entry, NukiBridge bridge)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
bool _register_webhook(NukiBridge bridge, str entry_id, str url)
web.Response handle_webhook(HomeAssistant hass, str webhook_id, web.Request request)
str get_url(HomeAssistant hass, *bool require_current_request=False, bool require_ssl=False, bool require_standard_port=False, bool require_cloud=False, bool allow_internal=True, bool allow_external=True, bool allow_cloud=True, bool|None allow_ip=None, bool|None prefer_external=None, bool prefer_cloud=False)