1 """The Netatmo integration."""
3 from __future__
import annotations
5 from http
import HTTPStatus
15 async_generate_url
as webhook_generate_url,
16 async_register
as webhook_register,
17 async_unregister
as webhook_unregister,
25 config_entry_oauth2_flow,
26 config_validation
as cv,
50 from .data_handler
import NetatmoDataHandler
51 from .webhook
import async_handle_webhook
53 _LOGGER = logging.getLogger(__name__)
55 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
57 MAX_WEBHOOK_RETRIES = 3
60 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
61 """Set up the Netatmo component."""
75 """Set up Netatmo from a config entry."""
77 await config_entry_oauth2_flow.async_get_config_entry_implementation(
83 if not entry.unique_id:
84 hass.config_entries.async_update_entry(entry, unique_id=DOMAIN)
86 session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
88 await session.async_ensure_token_valid()
89 except aiohttp.ClientResponseError
as ex:
90 _LOGGER.warning(
"API error: %s (%s)", ex.status, ex.message)
92 HTTPStatus.BAD_REQUEST,
93 HTTPStatus.UNAUTHORIZED,
97 raise ConfigEntryNotReady
from ex
99 required_scopes = api.get_api_scopes(entry.data[
"auth_implementation"])
100 if not (set(session.token[
"scope"]) & set(required_scopes)):
102 "Session is missing scopes: %s",
103 set(required_scopes) - set(session.token[
"scope"]),
107 hass.data[DOMAIN][entry.entry_id] = {
109 aiohttp_client.async_get_clientsession(hass), session
114 hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler
115 await data_handler.async_setup()
117 async
def unregister_webhook(
120 if CONF_WEBHOOK_ID
not in entry.data:
122 _LOGGER.debug(
"Unregister Netatmo webhook (%s)", entry.data[CONF_WEBHOOK_ID])
125 f
"signal-{DOMAIN}-webhook-None",
126 {
"type":
"None",
"data": {WEBHOOK_PUSH_TYPE: WEBHOOK_DEACTIVATION}},
128 webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
130 await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook()
131 except pyatmo.ApiError:
133 "No webhook to be dropped for %s", entry.data[CONF_WEBHOOK_ID]
136 async
def register_webhook(
139 if CONF_WEBHOOK_ID
not in entry.data:
140 data = {**entry.data, CONF_WEBHOOK_ID: secrets.token_hex()}
141 hass.config_entries.async_update_entry(entry, data=data)
143 if cloud.async_active_subscription(hass):
146 webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID])
149 "auth_implementation"
150 ] == cloud.DOMAIN
and not webhook_url.startswith(
"https://"):
152 "Webhook not registered - "
153 "https and port 443 is required to register the webhook"
161 entry.data[CONF_WEBHOOK_ID],
162 async_handle_webhook,
166 await hass.data[DOMAIN][entry.entry_id][AUTH].async_addwebhook(webhook_url)
167 _LOGGER.debug(
"Register Netatmo webhook: %s", webhook_url)
168 except pyatmo.ApiError
as err:
169 _LOGGER.error(
"Error during webhook registration - %s", err)
171 entry.async_on_unload(
172 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
175 async
def manage_cloudhook(state: cloud.CloudConnectionState) ->
None:
176 if state
is cloud.CloudConnectionState.CLOUD_CONNECTED:
177 await register_webhook(
None)
179 if state
is cloud.CloudConnectionState.CLOUD_DISCONNECTED:
180 await unregister_webhook(
None)
183 if cloud.async_active_subscription(hass):
184 if cloud.async_is_connected(hass):
185 await register_webhook(
None)
186 entry.async_on_unload(
187 cloud.async_listen_connection_change(hass, manage_cloudhook)
192 hass.services.async_register(DOMAIN,
"register_webhook", register_webhook)
193 hass.services.async_register(DOMAIN,
"unregister_webhook", unregister_webhook)
195 entry.async_on_unload(entry.add_update_listener(async_config_entry_updated))
201 """Generate the full URL for a webhook_id."""
202 if CONF_CLOUDHOOK_URL
not in entry.data:
203 webhook_url = await cloud.async_create_cloudhook(
204 hass, entry.data[CONF_WEBHOOK_ID]
206 data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url}
207 hass.config_entries.async_update_entry(entry, data=data)
209 return str(entry.data[CONF_CLOUDHOOK_URL])
213 """Handle signals of config entry being updated."""
218 """Unload a config entry."""
219 data = hass.data[DOMAIN]
221 if CONF_WEBHOOK_ID
in entry.data:
222 webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
224 await data[entry.entry_id][AUTH].async_dropwebhook()
225 except pyatmo.ApiError:
226 _LOGGER.debug(
"No webhook to be dropped")
227 _LOGGER.debug(
"Unregister Netatmo webhook")
229 unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
231 if unload_ok
and entry.entry_id
in data:
232 data.pop(entry.entry_id)
238 """Cleanup when entry is removed."""
239 if CONF_WEBHOOK_ID
in entry.data
and cloud.async_active_subscription(hass):
242 "Removing Netatmo cloudhook (%s)", entry.data[CONF_WEBHOOK_ID]
244 await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID])
250 hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
252 """Remove a config entry from a device."""
253 data = hass.data[DOMAIN][config_entry.entry_id][DATA_HANDLER]
254 modules = [m
for h
in data.account.homes.values()
for m
in h.modules]
255 rooms = [r
for h
in data.account.homes.values()
for r
in h.rooms]
259 for identifier
in device_entry.identifiers
260 if identifier[0] == DOMAIN
261 and identifier[1]
in modules
262 or identifier[1]
in rooms
str async_cloudhook_generate_url(HomeAssistant hass, ConfigEntry entry)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_remove_config_entry_device(HomeAssistant hass, ConfigEntry config_entry, DeviceEntry device_entry)
None async_remove_entry(HomeAssistant hass, ConfigEntry entry)
None async_config_entry_updated(HomeAssistant hass, ConfigEntry entry)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
CALLBACK_TYPE async_at_started(HomeAssistant hass, Callable[[HomeAssistant], Coroutine[Any, Any, None]|None] at_start_cb)