async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up RenoWeb platforms as config entry.""" if not entry.options: hass.config_entries.async_update_entry( entry, options={ CONF_UPDATE_INTERVAL: entry.data.get( CONF_UPDATE_INTERVAL, DEFAULT_SCAN_INTERVAL ), }, ) session = aiohttp_client.async_get_clientsession(hass) renoweb = RenoWebData( API_KEY, entry.data.get(CONF_MUNICIPALITY_ID), entry.data.get(CONF_ADDRESS_ID), session, ) _LOGGER.debug("Connected to RenoWeb Platform") hass.data.setdefault(DOMAIN, {})[entry.entry_id] = renoweb coordinator = DataUpdateCoordinator( hass, _LOGGER, name=DOMAIN, update_method=renoweb.get_pickup_data, update_interval=timedelta( hours=entry.options.get(CONF_UPDATE_INTERVAL, DEFAULT_SCAN_INTERVAL) ), ) try: await renoweb.get_pickup_data() except InvalidApiKey: _LOGGER.error( "Could not Authorize against Weatherflow Server. Please reinstall integration." ) return except (ResultError, ServerDisconnectedError) as err: _LOGGER.warning(str(err)) raise ConfigEntryNotReady except RequestError as err: _LOGGER.error(f"Error occured: {err}") return # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, "renoweb": renoweb, "municipality_id": entry.data.get(CONF_MUNICIPALITY_ID), "address_id": entry.data.get(CONF_ADDRESS_ID), } await _async_get_or_create_renoweb_device_in_registry( hass, entry, entry.data.get(CONF_ADDRESS_ID) ) for platform in INTEGRATION_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) if not entry.update_listeners: entry.add_update_listener(async_update_options) return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sense from a config entry.""" entry_data = entry.data email = entry_data[CONF_EMAIL] password = entry_data[CONF_PASSWORD] timeout = entry_data[CONF_TIMEOUT] client_session = async_get_clientsession(hass) gateway = ASyncSenseable(api_timeout=timeout, wss_timeout=timeout, client_session=client_session) gateway.rate_limit = ACTIVE_UPDATE_RATE try: await gateway.authenticate(email, password) except SenseAuthenticationException: _LOGGER.error("Could not authenticate with sense server") return False except SENSE_TIMEOUT_EXCEPTIONS as err: raise ConfigEntryNotReady( str(err) or "Timed out during authentication") from err except SENSE_EXCEPTIONS as err: raise ConfigEntryNotReady(str(err) or "Error during authentication") from err sense_devices_data = SenseDevicesData() try: sense_discovered_devices = await gateway.get_discovered_device_data() await gateway.update_realtime() except SENSE_TIMEOUT_EXCEPTIONS as err: raise ConfigEntryNotReady( str(err) or "Timed out during realtime update") from err except SENSE_EXCEPTIONS as err: raise ConfigEntryNotReady(str(err) or "Error during realtime update") from err trends_coordinator: DataUpdateCoordinator[None] = DataUpdateCoordinator( hass, _LOGGER, name=f"Sense Trends {email}", update_method=gateway.update_trend_data, update_interval=timedelta(seconds=300), ) # Start out as unavailable so we do not report 0 data # until the update happens trends_coordinator.last_update_success = False # This can take longer than 60s and we already know # sense is online since get_discovered_device_data was # successful so we do it later. asyncio.create_task(trends_coordinator.async_request_refresh()) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { SENSE_DATA: gateway, SENSE_DEVICES_DATA: sense_devices_data, SENSE_TRENDS_COORDINATOR: trends_coordinator, SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def async_sense_update(_): """Retrieve latest state.""" try: await gateway.update_realtime() except SENSE_TIMEOUT_EXCEPTIONS as ex: _LOGGER.error("Timeout retrieving data: %s", ex) except SENSE_EXCEPTIONS as ex: _LOGGER.error("Failed to update data: %s", ex) data = gateway.get_realtime() if "devices" in data: sense_devices_data.set_devices_data(data["devices"]) async_dispatcher_send( hass, f"{SENSE_DEVICE_UPDATE}-{gateway.sense_monitor_id}") remove_update_callback = async_track_time_interval( hass, async_sense_update, timedelta(seconds=ACTIVE_UPDATE_RATE)) @callback def _remove_update_callback_at_stop(event): remove_update_callback() entry.async_on_unload(remove_update_callback) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _remove_update_callback_at_stop)) return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" _LOGGER.info( "Version %s is starting, if you have any issues please report" " them here: %s", VERSION, ISSUE_URL, ) hass.data.setdefault(DOMAIN, {}) updated_config = config_entry.data.copy() # Set amazon fwd blank if missing if CONF_AMAZON_FWDS not in updated_config.keys(): updated_config[CONF_AMAZON_FWDS] = [] # Set default timeout if missing if CONF_IMAP_TIMEOUT not in updated_config.keys(): updated_config[CONF_IMAP_TIMEOUT] = DEFAULT_IMAP_TIMEOUT # Set external path off by default if CONF_ALLOW_EXTERNAL not in config_entry.data.keys(): updated_config[CONF_ALLOW_EXTERNAL] = False updated_config[CONF_PATH] = default_image_path(hass, config_entry) # Set image security always on if CONF_IMAGE_SECURITY not in config_entry.data.keys(): updated_config[CONF_IMAGE_SECURITY] = True # Sort the resources updated_config[CONF_RESOURCES] = sorted(updated_config[CONF_RESOURCES]) if updated_config != config_entry.data: hass.config_entries.async_update_entry(config_entry, data=updated_config) config_entry.add_update_listener(update_listener) config_entry.options = config_entry.data config = config_entry.data async def async_update_data(): """Fetch data """ async with async_timeout.timeout(config.get(CONF_IMAP_TIMEOUT)): return await hass.async_add_executor_job(process_emails, hass, config) coordinator = DataUpdateCoordinator( hass, _LOGGER, name=f"Mail and Packages ({config.get(CONF_HOST)})", update_method=async_update_data, update_interval=timedelta( minutes=config_entry.data.get(CONF_SCAN_INTERVAL)), ) # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() hass.data[DOMAIN][config_entry.entry_id] = { COORDINATOR: coordinator, } try: hass.async_create_task( hass.config_entries.async_forward_entry_setup( config_entry, PLATFORM)) except ValueError: pass return True
async def setup_alexa(hass, config_entry, login_obj): """Set up a alexa api based on host parameter.""" async def async_update_data(): """Fetch data from API endpoint. This is the place to pre-process the data to lookup tables so entities can quickly look up their data. This will ping Alexa API to identify all devices, bluetooth, and the last called device. This will add new devices and services when discovered. By default this runs every SCAN_INTERVAL seconds unless another method calls it. if websockets is connected, it will increase the delay 10-fold between updates. While throttled at MIN_TIME_BETWEEN_SCANS, care should be taken to reduce the number of runs to avoid flooding. Slow changing states should be checked here instead of in spawned components like media_player since this object is one per account. Each AlexaAPI call generally results in two webpage requests. """ from alexapy import AlexaAPI email: Text = login_obj.email if email not in hass.data[DATA_ALEXAMEDIA]["accounts"]: return existing_serials = _existing_serials(hass, login_obj) existing_entities = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "entities"]["media_player"].values() auth_info = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( "auth_info") new_devices = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "new_devices"] devices = {} bluetooth = {} preferences = {} dnd = {} raw_notifications = {} tasks = [ AlexaAPI.get_devices(login_obj), AlexaAPI.get_bluetooth(login_obj), AlexaAPI.get_device_preferences(login_obj), AlexaAPI.get_dnd_state(login_obj), AlexaAPI.get_notifications(login_obj), ] if new_devices: tasks.append(AlexaAPI.get_authentication(login_obj)) try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(10): if new_devices: ( devices, bluetooth, preferences, dnd, raw_notifications, auth_info, ) = await asyncio.gather(*tasks) else: ( devices, bluetooth, preferences, dnd, raw_notifications, ) = await asyncio.gather(*tasks) _LOGGER.debug( "%s: Found %s devices, %s bluetooth", hide_email(email), len(devices) if devices is not None else "", len(bluetooth.get("bluetoothStates", [])) if bluetooth is not None else "", ) if (devices is None or bluetooth is None) and not (hass.data[ DATA_ALEXAMEDIA]["accounts"][email]["configurator"]): raise AlexapyLoginError() except (AlexapyLoginError, RuntimeError, JSONDecodeError): _LOGGER.debug("%s: Alexa API disconnected; attempting to relogin", hide_email(email)) await login_obj.login_with_cookie() await test_login_status(hass, config_entry, login_obj, setup_alexa) return except BaseException as err: raise UpdateFailed(f"Error communicating with API: {err}") await process_notifications(login_obj, raw_notifications) # Process last_called data to fire events await update_last_called(login_obj) new_alexa_clients = [] # list of newly discovered device names exclude_filter = [] include_filter = [] for device in devices: serial = device["serialNumber"] dev_name = device["accountName"] if include and dev_name not in include: include_filter.append(dev_name) if "appDeviceList" in device: for app in device["appDeviceList"]: (hass.data[DATA_ALEXAMEDIA]["accounts"][email] ["excluded"][app["serialNumber"]]) = device hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ serial] = device continue elif exclude and dev_name in exclude: exclude_filter.append(dev_name) if "appDeviceList" in device: for app in device["appDeviceList"]: (hass.data[DATA_ALEXAMEDIA]["accounts"][email] ["excluded"][app["serialNumber"]]) = device hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ serial] = device continue if "bluetoothStates" in bluetooth: for b_state in bluetooth["bluetoothStates"]: if serial == b_state["deviceSerialNumber"]: device["bluetooth_state"] = b_state break if "devicePreferences" in preferences: for dev in preferences["devicePreferences"]: if dev["deviceSerialNumber"] == serial: device["locale"] = dev["locale"] device["timeZoneId"] = dev["timeZoneId"] _LOGGER.debug( "%s: Locale %s timezone %s", dev_name, device["locale"], device["timeZoneId"], ) break if "doNotDisturbDeviceStatusList" in dnd: for dev in dnd["doNotDisturbDeviceStatusList"]: if dev["deviceSerialNumber"] == serial: device["dnd"] = dev["enabled"] _LOGGER.debug("%s: DND %s", dev_name, device["dnd"]) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "devices"]["switch"].setdefault( serial, {"dnd": True}) break hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "auth_info"] = device["auth_info"] = auth_info hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ "media_player"][serial] = device if serial not in existing_serials: new_alexa_clients.append(dev_name) elif serial in existing_entities: await hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "entities"]["media_player"].get(serial).refresh( device, no_api=True) _LOGGER.debug( "%s: Existing: %s New: %s;" " Filtered out by not being in include: %s " "or in exclude: %s", hide_email(email), list(existing_entities), new_alexa_clients, include_filter, exclude_filter, ) if new_alexa_clients: cleaned_config = config.copy() cleaned_config.pop(CONF_PASSWORD, None) # CONF_PASSWORD contains sensitive info which is no longer needed for component in ALEXA_COMPONENTS: _LOGGER.debug("Loading %s", component) hass.async_add_job( hass.config_entries.async_forward_entry_setup( config_entry, component)) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False async def process_notifications(login_obj, raw_notifications=None): """Process raw notifications json.""" from alexapy import AlexaAPI if not raw_notifications: raw_notifications = await AlexaAPI.get_notifications(login_obj) email: Text = login_obj.email notifications = {"process_timestamp": datetime.utcnow()} for notification in raw_notifications: n_dev_id = notification.get("deviceSerialNumber") if n_dev_id is None: # skip notifications untied to a device for now # https://github.com/custom-components/alexa_media_player/issues/633#issuecomment-610705651 continue n_type = notification.get("type") if n_type is None: continue if n_type == "MusicAlarm": n_type = "Alarm" n_id = notification["notificationIndex"] if n_type == "Alarm": n_date = notification.get("originalDate") n_time = notification.get("originalTime") notification["date_time"] = (f"{n_date} {n_time}" if n_date and n_time else None) if n_dev_id not in notifications: notifications[n_dev_id] = {} if n_type not in notifications[n_dev_id]: notifications[n_dev_id][n_type] = {} notifications[n_dev_id][n_type][n_id] = notification hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "notifications"] = notifications _LOGGER.debug( "%s: Updated %s notifications for %s devices at %s", hide_email(email), len(raw_notifications), len(notifications), dt.as_local(hass.data[DATA_ALEXAMEDIA]["accounts"][email] ["notifications"]["process_timestamp"]), ) async def update_last_called(login_obj, last_called=None): """Update the last called device for the login_obj. This will store the last_called in hass.data and also fire an event to notify listeners. """ from alexapy import AlexaAPI if not last_called: last_called = await AlexaAPI.get_last_device_serial(login_obj) _LOGGER.debug("%s: Updated last_called: %s", hide_email(email), hide_serial(last_called)) stored_data = hass.data[DATA_ALEXAMEDIA]["accounts"][email] if ("last_called" in stored_data and last_called != stored_data["last_called"]) or ( "last_called" not in stored_data and last_called is not None): _LOGGER.debug( "%s: last_called changed: %s to %s", hide_email(email), hide_serial(stored_data["last_called"] if "last_called" in stored_data else None), hide_serial(last_called), ) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"last_called_change": last_called}, ) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "last_called"] = last_called async def update_bluetooth_state(login_obj, device_serial): """Update the bluetooth state on ws bluetooth event.""" from alexapy import AlexaAPI bluetooth = await AlexaAPI.get_bluetooth(login_obj) device = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ "media_player"][device_serial] if "bluetoothStates" in bluetooth: for b_state in bluetooth["bluetoothStates"]: if device_serial == b_state["deviceSerialNumber"]: # _LOGGER.debug("%s: setting value for: %s to %s", # hide_email(email), # hide_serial(device_serial), # hide_serial(b_state)) device["bluetooth_state"] = b_state return device["bluetooth_state"] _LOGGER.debug( "%s: get_bluetooth for: %s failed with %s", hide_email(email), hide_serial(device_serial), hide_serial(bluetooth), ) return None async def clear_history(call): """Handle clear history service request. Arguments call.ATTR_EMAIL {List[str: None]} -- Case-sensitive Alexa emails. Default is all known emails. call.ATTR_NUM_ENTRIES {int: 50} -- Number of entries to delete. Returns bool -- True if deletion successful """ from alexapy import AlexaAPI requested_emails = call.data.get(ATTR_EMAIL) items: int = int(call.data.get(ATTR_NUM_ENTRIES)) _LOGGER.debug("Service clear_history called for: %i items for %s", items, requested_emails) for email, account_dict in hass.data[DATA_ALEXAMEDIA][ "accounts"].items(): if requested_emails and email not in requested_emails: continue login_obj = account_dict["login_obj"] return await AlexaAPI.clear_history(login_obj, items) async def last_call_handler(call): """Handle last call service request. Args: call.ATTR_EMAIL: List of case-sensitive Alexa email addresses. If None all accounts are updated. """ requested_emails = call.data.get(ATTR_EMAIL) _LOGGER.debug("Service update_last_called for: %s", requested_emails) for email, account_dict in hass.data[DATA_ALEXAMEDIA][ "accounts"].items(): if requested_emails and email not in requested_emails: continue login_obj = account_dict["login_obj"] await update_last_called(login_obj) async def ws_connect() -> WebsocketEchoClient: """Open WebSocket connection. This will only attempt one login before failing. """ websocket: Optional[WebsocketEchoClient] = None try: websocket = WebsocketEchoClient( login_obj, ws_handler, ws_open_handler, ws_close_handler, ws_error_handler, ) _LOGGER.debug("%s: Websocket created: %s", hide_email(email), websocket) await websocket.async_run() except BaseException as exception_: # pylint: disable=broad-except _LOGGER.debug("%s: Websocket creation failed: %s", hide_email(email), exception_) return return websocket async def ws_handler(message_obj): """Handle websocket messages. This allows push notifications from Alexa to update last_called and media state. """ command = (message_obj.json_payload["command"] if isinstance(message_obj.json_payload, dict) and "command" in message_obj.json_payload else None) json_payload = (message_obj.json_payload["payload"] if isinstance(message_obj.json_payload, dict) and "payload" in message_obj.json_payload else None) existing_serials = _existing_serials(hass, login_obj) seen_commands = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_commands"] if command and json_payload: _LOGGER.debug( "%s: Received websocket command: %s : %s", hide_email(email), command, hide_serial(json_payload), ) serial = None if command not in seen_commands: seen_commands[command] = time.time() _LOGGER.debug("Adding %s to seen_commands: %s", command, seen_commands) if ("dopplerId" in json_payload and "deviceSerialNumber" in json_payload["dopplerId"]): serial = json_payload["dopplerId"]["deviceSerialNumber"] elif ("key" in json_payload and "entryId" in json_payload["key"] and json_payload["key"]["entryId"].find("#") != -1): serial = (json_payload["key"]["entryId"]).split("#")[2] json_payload["key"]["serialNumber"] = serial else: serial = None if command == "PUSH_ACTIVITY": # Last_Alexa Updated last_called = { "serialNumber": serial, "timestamp": json_payload["timestamp"], } if serial and serial in existing_serials: await update_last_called(login_obj, last_called) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"push_activity": json_payload}, ) elif command in ( "PUSH_AUDIO_PLAYER_STATE", "PUSH_MEDIA_CHANGE", "PUSH_MEDIA_PROGRESS_CHANGE", ): # Player update/ Push_media from tune_in if serial and serial in existing_serials: _LOGGER.debug("Updating media_player: %s", hide_serial(json_payload)) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command == "PUSH_VOLUME_CHANGE": # Player volume update if serial and serial in existing_serials: _LOGGER.debug("Updating media_player volume: %s", hide_serial(json_payload)) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command in ( "PUSH_DOPPLER_CONNECTION_CHANGE", "PUSH_EQUALIZER_STATE_CHANGE", ): # Player availability update if serial and serial in existing_serials: _LOGGER.debug( "Updating media_player availability %s", hide_serial(json_payload), ) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command == "PUSH_BLUETOOTH_STATE_CHANGE": # Player bluetooth update bt_event = json_payload["bluetoothEvent"] bt_success = json_payload["bluetoothEventSuccess"] if (serial and serial in existing_serials and bt_success and bt_event and bt_event in ["DEVICE_CONNECTED", "DEVICE_DISCONNECTED"]): _LOGGER.debug("Updating media_player bluetooth %s", hide_serial(json_payload)) bluetooth_state = await update_bluetooth_state( login_obj, serial) # _LOGGER.debug("bluetooth_state %s", # hide_serial(bluetooth_state)) if bluetooth_state: async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"bluetooth_change": bluetooth_state}, ) elif command == "PUSH_MEDIA_QUEUE_CHANGE": # Player availability update if serial and serial in existing_serials: _LOGGER.debug("Updating media_player queue %s", hide_serial(json_payload)) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"queue_state": json_payload}, ) elif command == "PUSH_NOTIFICATION_CHANGE": # Player update await process_notifications(login_obj) if serial and serial in existing_serials: _LOGGER.debug( "Updating mediaplayer notifications: %s", hide_serial(json_payload), ) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"notification_update": json_payload}, ) elif command in [ "PUSH_DELETE_DOPPLER_ACTIVITIES", # delete Alexa history "PUSH_LIST_ITEM_CHANGE", # update shopping list "PUSH_CONTENT_FOCUS_CHANGE", # likely prime related refocus ]: pass else: _LOGGER.warning( "Unhandled command: %s with data %s. Please report at %s", command, hide_serial(json_payload), ISSUE_URL, ) if serial in existing_serials: hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_activity"]["serials"][serial] = time.time() if (serial and serial not in existing_serials and serial not in (hass.data[DATA_ALEXAMEDIA]["accounts"] [email]["excluded"].keys())): _LOGGER.debug("Discovered new media_player %s", serial) (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] ) = True coordinator = hass.data[DATA_ALEXAMEDIA]["accounts"][ email].get("coordinator") if coordinator: await coordinator.async_request_refresh() async def ws_open_handler(): """Handle websocket open.""" import time email: Text = login_obj.email _LOGGER.debug("%s: Websocket succesfully connected", hide_email(email)) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocketerror"] = 0 # set errors to 0 hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_lastattempt"] = time.time() async def ws_close_handler(): """Handle websocket close. This should attempt to reconnect up to 5 times """ from asyncio import sleep import time email: Text = login_obj.email errors: int = ( hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"]) delay: int = 5 * 2**errors last_attempt = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_lastattempt"] now = time.time() if (now - last_attempt) < delay: return while errors < 5 and not ( hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]): _LOGGER.debug( "%s: Websocket closed; reconnect #%i in %is", hide_email(email), errors, delay, ) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_lastattempt"] = time.time() hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket"] = await ws_connect() errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocketerror"] = (hass.data[DATA_ALEXAMEDIA]["accounts"] [email]["websocketerror"] + 1) delay = 5 * 2**errors await sleep(delay) errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocketerror"] _LOGGER.debug("%s: Websocket closed; retries exceeded; polling", hide_email(email)) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] = None coordinator = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( "coordinator") if coordinator: coordinator.update_interval = scan_interval await coordinator.async_request_refresh() async def ws_error_handler(message): """Handle websocket error. This currently logs the error. In the future, this should invalidate the websocket and determine if a reconnect should be done. By specification, websockets will issue a close after every error. """ email: Text = login_obj.email errors = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocketerror"] _LOGGER.debug( "%s: Received websocket error #%i %s: type %s", hide_email(email), errors, message, type(message), ) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] = None if message == "<class 'aiohttp.streams.EofStream'>": hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"] = 5 _LOGGER.debug("%s: Immediate abort on EoFstream", hide_email(email)) return hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocketerror"] = errors + 1 config = config_entry.data email = config.get(CONF_EMAIL) include = config.get(CONF_INCLUDE_DEVICES) exclude = config.get(CONF_EXCLUDE_DEVICES) scan_interval: float = (config.get(CONF_SCAN_INTERVAL).total_seconds() if isinstance(config.get(CONF_SCAN_INTERVAL), timedelta) else config.get(CONF_SCAN_INTERVAL)) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"] = login_obj websocket_enabled = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket"] = await ws_connect() coordinator = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( "coordinator") if coordinator is None: _LOGGER.debug("Creating coordinator") hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "coordinator"] = coordinator = DataUpdateCoordinator( hass, _LOGGER, # Name of the data. For logging purposes. name="alexa_media", update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. update_interval=timedelta( seconds=scan_interval * 10 if websocket_enabled else scan_interval), ) # Fetch initial data so we have data when entities subscribe _LOGGER.debug("Refreshing coordinator") await coordinator.async_refresh() hass.services.async_register( DOMAIN, SERVICE_UPDATE_LAST_CALLED, last_call_handler, schema=LAST_CALL_UPDATE_SCHEMA, ) hass.services.async_register(DOMAIN, SERVICE_CLEAR_HISTORY, clear_history, schema=CLEAR_HISTORY_SCHEMA) # Clear configurator. We delay till here to avoid leaving a modal orphan await clear_configurator(hass, email) return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Spotify from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) try: await session.async_ensure_token_valid() except aiohttp.ClientError as err: raise ConfigEntryNotReady from err spotify = Spotify(auth=session.token["access_token"]) try: current_user = await hass.async_add_executor_job(spotify.me) except SpotifyException as err: raise ConfigEntryNotReady from err if not current_user: raise ConfigEntryNotReady async def _update_devices() -> list[dict[str, Any]]: if not session.valid_token: await session.async_ensure_token_valid() await hass.async_add_executor_job( spotify.set_auth, session.token["access_token"] ) try: devices: dict[str, Any] | None = await hass.async_add_executor_job( spotify.devices ) except (requests.RequestException, SpotifyException) as err: raise UpdateFailed from err if devices is None: return [] return devices.get("devices", []) device_coordinator: DataUpdateCoordinator[ list[dict[str, Any]] ] = DataUpdateCoordinator( hass, LOGGER, name=f"{entry.title} Devices", update_interval=timedelta(minutes=5), update_method=_update_devices, ) await device_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = HomeAssistantSpotifyData( client=spotify, current_user=current_user, devices=device_coordinator, session=session, ) if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): raise ConfigEntryAuthFailed hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Synology DSM sensors.""" # Migrate device indentifiers dev_reg = await get_dev_reg(hass) devices: list[DeviceEntry] = device_registry.async_entries_for_config_entry( dev_reg, entry.entry_id ) for device in devices: old_identifier = list(next(iter(device.identifiers))) if len(old_identifier) > 2: new_identifier = { (old_identifier.pop(0), "_".join([str(x) for x in old_identifier])) } _LOGGER.debug( "migrate identifier '%s' to '%s'", device.identifiers, new_identifier ) dev_reg.async_update_device(device.id, new_identifiers=new_identifier) # Migrate existing entry configuration if entry.data.get(CONF_VERIFY_SSL) is None: hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL} ) # Continue setup api = SynoApi(hass, entry) try: await api.async_setup() except ( SynologyDSMLogin2SARequiredException, SynologyDSMLoginDisabledAccountException, SynologyDSMLoginInvalidException, SynologyDSMLoginPermissionDeniedException, ) as err: if err.args[0] and isinstance(err.args[0], dict): # pylint: disable=no-member details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryAuthFailed(f"reason: {details}") from err except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: if err.args[0] and isinstance(err.args[0], dict): # pylint: disable=no-member details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryNotReady(details) from err hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = { UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), SYNO_API: api, SYSTEM_LOADED: True, } # Services await _async_setup_services(hass) # For SSDP compat if not entry.data.get(CONF_MAC): network = await hass.async_add_executor_job(getattr, api.dsm, "network") hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_MAC: network.macs} ) async def async_coordinator_update_data_cameras() -> dict[ str, dict[str, SynoCamera] ] | None: """Fetch all camera data from api.""" if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: raise UpdateFailed("System not fully loaded") if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: return None surveillance_station = api.surveillance_station try: async with async_timeout.timeout(30): await hass.async_add_executor_job(surveillance_station.update) except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err return { "cameras": { camera.id: camera for camera in surveillance_station.get_all_cameras() } } async def async_coordinator_update_data_central() -> None: """Fetch all device and sensor data from api.""" try: await api.async_update() except Exception as err: raise UpdateFailed(f"Error communicating with API: {err}") from err return None async def async_coordinator_update_data_switches() -> dict[ str, dict[str, Any] ] | None: """Fetch all switch data from api.""" if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: raise UpdateFailed("System not fully loaded") if SynoSurveillanceStation.HOME_MODE_API_KEY not in api.dsm.apis: return None surveillance_station = api.surveillance_station return { "switches": { "home_mode": await hass.async_add_executor_job( surveillance_station.get_home_mode_status ) } } hass.data[DOMAIN][entry.unique_id][COORDINATOR_CAMERAS] = DataUpdateCoordinator( hass, _LOGGER, name=f"{entry.unique_id}_cameras", update_method=async_coordinator_update_data_cameras, update_interval=timedelta(seconds=30), ) hass.data[DOMAIN][entry.unique_id][COORDINATOR_CENTRAL] = DataUpdateCoordinator( hass, _LOGGER, name=f"{entry.unique_id}_central", update_method=async_coordinator_update_data_central, update_interval=timedelta( minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ), ) hass.data[DOMAIN][entry.unique_id][COORDINATOR_SWITCHES] = DataUpdateCoordinator( hass, _LOGGER, name=f"{entry.unique_id}_switches", update_method=async_coordinator_update_data_switches, update_interval=timedelta(seconds=30), ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Eco-Devices from a config entry.""" config = entry.data options = entry.options scan_interval = options.get(CONF_SCAN_INTERVAL, config.get(CONF_SCAN_INTERVAL)) username = options.get(CONF_USERNAME, config.get(CONF_USERNAME)) password = options.get(CONF_PASSWORD, config.get(CONF_PASSWORD)) session = async_get_clientsession(hass, False) controller = EcoDevices( config[CONF_HOST], config[CONF_PORT], username, password, session=session, ) try: await controller.get_info() except EcoDevicesCannotConnectError as exception: raise ConfigEntryNotReady from exception async def async_update_data(): """Fetch data from API.""" try: return await controller.global_get() except EcoDevicesInvalidAuthError as err: raise UpdateFailed("Authentication error on Eco-Devices") from err except EcoDevicesCannotConnectError as err: raise UpdateFailed( f"Failed to communicating with API: {err}") from err coordinator = DataUpdateCoordinator( hass, _LOGGER, name=DOMAIN, update_method=async_update_data, update_interval=timedelta(seconds=scan_interval), ) await coordinator.async_refresh() if not coordinator.last_update_success: raise ConfigEntryNotReady undo_listener = entry.add_update_listener(_async_update_listener) hass.data[DOMAIN][entry.entry_id] = { CONTROLLER: controller, COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: undo_listener, } device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, controller.mac_address)}, manufacturer="GCE", model="Eco-Devices", default_name=f"Eco-Devices {controller.host}:{str(controller.port)}", sw_version=controller.version, connections={(dr.CONNECTION_NETWORK_MAC, controller.mac_address)}, configuration_url=f"http://{config[CONF_HOST]}:{config[CONF_PORT]}", ) for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform)) return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Somfy from a config entry.""" # Backwards compat if "auth_implementation" not in entry.data: hass.config_entries.async_update_entry( entry, data={ **entry.data, "auth_implementation": DOMAIN }) implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry)) data = hass.data[DOMAIN] data[API] = api.ConfigEntrySomfyApi(hass, entry, implementation) async def _update_all_devices(): """Update all the devices.""" devices = await hass.async_add_executor_job(data[API].get_devices) previous_devices = data[COORDINATOR].data # Sometimes Somfy returns an empty list. if not devices and previous_devices: raise UpdateFailed("No devices returned") return {dev.id: dev for dev in devices} coordinator = DataUpdateCoordinator( hass, _LOGGER, name="somfy device update", update_method=_update_all_devices, update_interval=SCAN_INTERVAL, ) data[COORDINATOR] = coordinator await coordinator.async_refresh() if all(not bool(device.states) for device in coordinator.data.values()): _LOGGER.debug( "All devices have assumed state. Update interval has been reduced to: %s", SCAN_INTERVAL_ALL_ASSUMED_STATE, ) coordinator.update_interval = SCAN_INTERVAL_ALL_ASSUMED_STATE device_registry = await dr.async_get_registry(hass) hubs = [ device for device in coordinator.data.values() if Category.HUB.value in device.categories ] for hub in hubs: device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, hub.id)}, manufacturer="Somfy", name=hub.name, model=hub.type, ) for component in SOMFY_COMPONENTS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component)) return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Toyota Connected Services from a config entry.""" if hass.data.get(DOMAIN) is None: hass.data.setdefault(DOMAIN, {}) _LOGGER.info(STARTUP_MESSAGE) email = entry.data[CONF_EMAIL] password = entry.data[CONF_PASSWORD] locale = entry.data[CONF_LOCALE] region = entry.data[CONF_REGION] client = MyT( username=email, password=password, locale=locale, region=region.lower(), ) await client.login() async def async_update_data(): """Fetch data from Toyota API.""" vehicles = [] try: vehicles = await client.gather_all_information() except ToyotaLoginError as ex: _LOGGER.error(ex) except Exception as ex: # pylint: disable=broad-except _LOGGER.error(ex) _LOGGER.debug(vehicles) return vehicles coordinator = DataUpdateCoordinator( hass, _LOGGER, name=DOMAIN, update_method=async_update_data, update_interval=UPDATE_INTERVAL, ) # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() hass.data[DOMAIN][entry.entry_id] = { DATA_CLIENT: client, DATA_COORDINATOR: coordinator, } if not coordinator.last_update_success: raise ConfigEntryNotReady # Setup components for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) return True
async def async_setup_entry(hass, config_entry): """Set up AirVisual as config entry.""" if CONF_API_KEY in config_entry.data: _standardize_geography_config_entry(hass, config_entry) websession = aiohttp_client.async_get_clientsession(hass) cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession) async def async_update_data(): """Get new data from the API.""" if CONF_CITY in config_entry.data: api_coro = cloud_api.air_quality.city( config_entry.data[CONF_CITY], config_entry.data[CONF_STATE], config_entry.data[CONF_COUNTRY], ) else: api_coro = cloud_api.air_quality.nearest_city( config_entry.data[CONF_LATITUDE], config_entry.data[CONF_LONGITUDE], ) try: return await api_coro except (InvalidKeyError, KeyExpiredError): matching_flows = [ flow for flow in hass.config_entries.flow.async_progress() if flow["context"]["source"] == SOURCE_REAUTH and flow["context"]["unique_id"] == config_entry.unique_id ] if not matching_flows: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={ "source": SOURCE_REAUTH, "unique_id": config_entry.unique_id, }, data=config_entry.data, )) return {} except AirVisualError as err: raise UpdateFailed( f"Error while retrieving data: {err}") from err coordinator = DataUpdateCoordinator( hass, LOGGER, name=async_get_geography_id(config_entry.data), # We give a placeholder update interval in order to create the coordinator; # then, below, we use the coordinator's presence (along with any other # coordinators using the same API key) to calculate an actual, leveled # update interval: update_interval=timedelta(minutes=5), update_method=async_update_data, ) hass.data[DOMAIN][DATA_COORDINATOR][ config_entry.entry_id] = coordinator async_sync_geo_coordinator_update_intervals( hass, config_entry.data[CONF_API_KEY]) # Only geography-based entries have options: hass.data[DOMAIN][DATA_LISTENER][ config_entry.entry_id] = config_entry.add_update_listener( async_reload_entry) else: _standardize_node_pro_config_entry(hass, config_entry) async def async_update_data(): """Get new data from the API.""" try: async with NodeSamba(config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD]) as node: return await node.async_get_latest_measurements() except NodeProError as err: raise UpdateFailed( f"Error while retrieving data: {err}") from err coordinator = DataUpdateCoordinator( hass, LOGGER, name="Node/Pro data", update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL, update_method=async_update_data, ) hass.data[DOMAIN][DATA_COORDINATOR][ config_entry.entry_id] = coordinator await coordinator.async_refresh() for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup( config_entry, component)) return True
async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Hue lights from a config entry.""" bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] light_coordinator = DataUpdateCoordinator( hass, _LOGGER, name="light", update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update), update_interval=SCAN_INTERVAL, request_refresh_debouncer=Debouncer( bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True ), ) # First do a refresh to see if we can reach the hub. # Otherwise we will declare not ready. await light_coordinator.async_refresh() if not light_coordinator.last_update_success: raise PlatformNotReady update_lights = partial( async_update_items, bridge, bridge.api.lights, {}, async_add_entities, partial(create_light, HueLight, light_coordinator, bridge, False), ) # We add a listener after fetching the data, so manually trigger listener bridge.reset_jobs.append(light_coordinator.async_add_listener(update_lights)) update_lights() api_version = tuple(int(v) for v in bridge.api.config.apiversion.split(".")) allow_groups = bridge.allow_groups if allow_groups and api_version < GROUP_MIN_API_VERSION: _LOGGER.warning("Please update your Hue bridge to support groups") allow_groups = False if not allow_groups: return group_coordinator = DataUpdateCoordinator( hass, _LOGGER, name="group", update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update), update_interval=SCAN_INTERVAL, request_refresh_debouncer=Debouncer( bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True ), ) update_groups = partial( async_update_items, bridge, bridge.api.groups, {}, async_add_entities, partial(create_light, HueLight, group_coordinator, bridge, True), ) bridge.reset_jobs.append(group_coordinator.async_add_listener(update_groups)) await group_coordinator.async_refresh()
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): """Set up Control4 lights from a config entry.""" entry_data = hass.data[DOMAIN][entry.entry_id] scan_interval = entry_data[CONF_SCAN_INTERVAL] _LOGGER.debug( "Scan interval = %s", scan_interval, ) async def async_update_data_non_dimmer(): """Fetch data from Control4 director for non-dimmer lights.""" try: return await director_update_data(hass, entry, CONTROL4_NON_DIMMER_VAR) except C4Exception as err: raise UpdateFailed(f"Error communicating with API: {err}") from err async def async_update_data_dimmer(): """Fetch data from Control4 director for dimmer lights.""" try: return await director_update_data(hass, entry, CONTROL4_DIMMER_VAR) except C4Exception as err: raise UpdateFailed(f"Error communicating with API: {err}") from err non_dimmer_coordinator = DataUpdateCoordinator( hass, _LOGGER, name="light", update_method=async_update_data_non_dimmer, update_interval=timedelta(seconds=scan_interval), ) dimmer_coordinator = DataUpdateCoordinator( hass, _LOGGER, name="light", update_method=async_update_data_dimmer, update_interval=timedelta(seconds=scan_interval), ) # Fetch initial data so we have data when entities subscribe await non_dimmer_coordinator.async_refresh() await dimmer_coordinator.async_refresh() items_of_category = await get_items_of_category(hass, entry, CONTROL4_CATEGORY) entity_list = [] for item in items_of_category: try: if item["type"] == CONTROL4_ENTITY_TYPE: item_name = item["name"] item_id = item["id"] item_parent_id = item["parentId"] item_manufacturer = None item_device_name = None item_model = None for parent_item in items_of_category: if parent_item["id"] == item_parent_id: item_manufacturer = parent_item["manufacturer"] item_device_name = parent_item["name"] item_model = parent_item["model"] else: continue except KeyError: _LOGGER.exception( "Unknown device properties received from Control4: %s", item, ) continue if item_id in dimmer_coordinator.data: item_is_dimmer = True item_coordinator = dimmer_coordinator elif item_id in non_dimmer_coordinator.data: item_is_dimmer = False item_coordinator = non_dimmer_coordinator else: director = entry_data[CONF_DIRECTOR] item_variables = await director.getItemVariables(item_id) _LOGGER.warning( "Couldn't get light state data for %s, skipping setup. Available variables from Control4: %s", item_name, item_variables, ) continue entity_list.append( Control4Light( entry_data, item_coordinator, item_name, item_id, item_device_name, item_manufacturer, item_model, item_parent_id, item_is_dimmer, )) async_add_entities(entity_list, True)
DailyParameters.WEATHER_CODE, DailyParameters.WIND_DIRECTION_10M_DOMINANT, DailyParameters.WIND_SPEED_10M_MAX, ], precipitation_unit=PrecipitationUnit.MILLIMETERS, temperature_unit=TemperatureUnit.CELSIUS, timezone="UTC", wind_speed_unit=WindSpeedUnit.KILOMETERS_PER_HOUR, ) except OpenMeteoError as err: raise UpdateFailed("Open-Meteo API communication error") from err coordinator: DataUpdateCoordinator[Forecast] = DataUpdateCoordinator( hass, LOGGER, name=f"{DOMAIN}_{entry.data[CONF_ZONE]}", update_interval=SCAN_INTERVAL, update_method=async_update_forecast, ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Open-Meteo config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( entry, PLATFORMS)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Network UPS Tools (NUT) from a config entry.""" # strip out the stale options CONF_RESOURCES, # maintain the entry in data in case of version rollback if CONF_RESOURCES in entry.options: new_data = { **entry.data, CONF_RESOURCES: entry.options[CONF_RESOURCES] } new_options = { k: v for k, v in entry.options.items() if k != CONF_RESOURCES } hass.config_entries.async_update_entry(entry, data=new_data, options=new_options) config = entry.data host = config[CONF_HOST] port = config[CONF_PORT] alias = config.get(CONF_ALIAS) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) data = PyNUTData(host, port, alias, username, password) async def async_update_data(): """Fetch data from NUT.""" async with async_timeout.timeout(10): await hass.async_add_executor_job(data.update) if not data.status: raise UpdateFailed("Error fetching UPS state") return data.status coordinator = DataUpdateCoordinator( hass, _LOGGER, name="NUT resource status", update_method=async_update_data, update_interval=timedelta(seconds=scan_interval), ) # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() status = coordinator.data _LOGGER.debug("NUT Sensors Available: %s", status) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) unique_id = _unique_id_from_status(status) if unique_id is None: unique_id = entry.entry_id hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, PYNUT_DATA: data, PYNUT_UNIQUE_ID: unique_id, } device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, unique_id)}, name=data.name.title(), manufacturer=data.device_info.get(ATTR_MANUFACTURER), model=data.device_info.get(ATTR_MODEL), sw_version=data.device_info.get(ATTR_SW_VERSION), ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True
azimuth=(entry.options[CONF_AZIMUTH] - 180), kwp=(entry.options[CONF_MODULES_POWER] / 1000), damping=entry.options.get(CONF_DAMPING, 0), inverter=inverter_size, ) # Free account have a resolution of 1 hour, using that as the default # update interval. Using a higher value for accounts with an API key. update_interval = timedelta(hours=1) if api_key is not None: update_interval = timedelta(minutes=30) coordinator: DataUpdateCoordinator[Estimate] = DataUpdateCoordinator( hass, logging.getLogger(__name__), name=DOMAIN, update_method=forecast.estimate, update_interval=update_interval, ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_update_data(): """Fetch data from the device using async_add_executor_job.""" try: async with async_timeout.timeout(10): state = await hass.async_add_executor_job(device.status) _LOGGER.debug("Got new state: %s", state) return state except DeviceException as ex: raise UpdateFailed(ex) from ex # Create update miio device and coordinator coordinator = DataUpdateCoordinator( hass, _LOGGER, name=name, update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. update_interval=timedelta(seconds=60), ) hass.data[DOMAIN][entry.entry_id] = { KEY_DEVICE: device, KEY_COORDINATOR: coordinator, } # Trigger first data fetch await coordinator.async_config_entry_first_refresh() async def async_setup_gateway_entry(hass: core.HomeAssistant, entry: config_entries.ConfigEntry): """Set up the Xiaomi Gateway component from a config entry."""
class DataManager: """Manage withing data.""" def __init__( self, hass: HomeAssistant, profile: str, api: ConfigEntryWithingsApi, user_id: int, webhook_config: WebhookConfig, ): """Initialize the data manager.""" self._hass = hass self._api = api self._user_id = user_id self._profile = profile self._webhook_config = webhook_config self._notify_subscribe_delay = datetime.timedelta(seconds=5) self._notify_unsubscribe_delay = datetime.timedelta(seconds=1) self._is_available = True self._cancel_interval_update_interval: Optional[CALLBACK_TYPE] = None self._cancel_configure_webhook_subscribe_interval: Optional[ CALLBACK_TYPE] = None self._api_notification_id = f"withings_{self._user_id}" self.subscription_update_coordinator = DataUpdateCoordinator( hass, _LOGGER, name="subscription_update_coordinator", update_interval=timedelta(minutes=120), update_method=self.async_subscribe_webhook, ) self.poll_data_update_coordinator = DataUpdateCoordinator( hass, _LOGGER, name="poll_data_update_coordinator", update_interval=timedelta(minutes=120) if self._webhook_config.enabled else timedelta(minutes=10), update_method=self.async_get_all_data, ) self.webhook_update_coordinator = WebhookUpdateCoordinator( self._hass, self._user_id) self._cancel_subscription_update: Optional[Callable[[], None]] = None self._subscribe_webhook_run_count = 0 @property def webhook_config(self) -> WebhookConfig: """Get the webhook config.""" return self._webhook_config @property def user_id(self) -> int: """Get the user_id of the authenticated user.""" return self._user_id @property def profile(self) -> str: """Get the profile.""" return self._profile def async_start_polling_webhook_subscriptions(self) -> None: """Start polling webhook subscriptions (if enabled) to reconcile their setup.""" self.async_stop_polling_webhook_subscriptions() def empty_listener() -> None: pass self._cancel_subscription_update = self.subscription_update_coordinator.async_add_listener( empty_listener) def async_stop_polling_webhook_subscriptions(self) -> None: """Stop polling webhook subscriptions.""" if self._cancel_subscription_update: self._cancel_subscription_update() self._cancel_subscription_update = None async def _do_retry(self, func, attempts=3) -> Any: """Retry a function call. Withings' API occasionally and incorrectly throws errors. Retrying the call tends to work. """ exception = None for attempt in range(1, attempts + 1): _LOGGER.debug("Attempt %s of %s", attempt, attempts) try: return await func() except Exception as exception1: # pylint: disable=broad-except await asyncio.sleep(0.1) exception = exception1 continue if exception: raise exception async def async_subscribe_webhook(self) -> None: """Subscribe the webhook to withings data updates.""" return await self._do_retry(self._async_subscribe_webhook) async def _async_subscribe_webhook(self) -> None: _LOGGER.debug("Configuring withings webhook") # On first startup, perform a fresh re-subscribe. Withings stops pushing data # if the webhook fails enough times but they don't remove the old subscription # config. This ensures the subscription is setup correctly and they start # pushing again. if self._subscribe_webhook_run_count == 0: _LOGGER.debug("Refreshing withings webhook configs") await self.async_unsubscribe_webhook() self._subscribe_webhook_run_count += 1 # Get the current webhooks. response = await self._hass.async_add_executor_job( self._api.notify_list) subscribed_applis = frozenset([ profile.appli for profile in response.profiles if profile.callbackurl == self._webhook_config.url ]) # Determine what subscriptions need to be created. ignored_applis = frozenset({NotifyAppli.USER}) to_add_applis = frozenset([ appli for appli in NotifyAppli if appli not in subscribed_applis and appli not in ignored_applis ]) # Subscribe to each one. for appli in to_add_applis: _LOGGER.debug( "Subscribing %s for %s in %s seconds", self._webhook_config.url, appli, self._notify_subscribe_delay.total_seconds(), ) # Withings will HTTP HEAD the callback_url and needs some downtime # between each call or there is a higher chance of failure. await asyncio.sleep(self._notify_subscribe_delay.total_seconds()) await self._hass.async_add_executor_job(self._api.notify_subscribe, self._webhook_config.url, appli) async def async_unsubscribe_webhook(self) -> None: """Unsubscribe webhook from withings data updates.""" return await self._do_retry(self._async_unsubscribe_webhook) async def _async_unsubscribe_webhook(self) -> None: # Get the current webhooks. response = await self._hass.async_add_executor_job( self._api.notify_list) # Revoke subscriptions. for profile in response.profiles: _LOGGER.debug( "Unsubscribing %s for %s in %s seconds", profile.callbackurl, profile.appli, self._notify_unsubscribe_delay.total_seconds(), ) # Quick calls to Withings can result in the service returning errors. Give them # some time to cool down. await asyncio.sleep(self._notify_subscribe_delay.total_seconds()) await self._hass.async_add_executor_job(self._api.notify_revoke, profile.callbackurl, profile.appli) async def async_get_all_data(self) -> Optional[Dict[MeasureType, Any]]: """Update all withings data.""" try: return await self._do_retry(self._async_get_all_data) except Exception as exception: # User is not authenticated. if isinstance( exception, (UnauthorizedException, AuthFailedException)) or NOT_AUTHENTICATED_ERROR.match( str(exception)): context = { const.PROFILE: self._profile, "userid": self._user_id, "source": "reauth", } # Check if reauth flow already exists. flow = next( iter(flow for flow in self._hass.config_entries.flow.async_progress() if flow.context == context), None, ) if flow: return # Start a reauth flow. await self._hass.config_entries.flow.async_init( const.DOMAIN, context=context, ) return raise exception async def _async_get_all_data(self) -> Optional[Dict[MeasureType, Any]]: _LOGGER.info("Updating all withings data") return { **await self.async_get_measures(), **await self.async_get_sleep_summary(), } async def async_get_measures(self) -> Dict[MeasureType, Any]: """Get the measures data.""" _LOGGER.debug("Updating withings measures") response = await self._hass.async_add_executor_job( self._api.measure_get_meas) # Sort from oldest to newest. groups = sorted( query_measure_groups(response, MeasureTypes.ANY, MeasureGroupAttribs.UNAMBIGUOUS), key=lambda group: group.created.datetime, reverse=False, ) return { WITHINGS_MEASURE_TYPE_MAP[measure.type].measurement: round(float(measure.value * pow(10, measure.unit)), 2) for group in groups for measure in group.measures } async def async_get_sleep_summary(self) -> Dict[MeasureType, Any]: """Get the sleep summary data.""" _LOGGER.debug("Updating withing sleep summary") now = dt.utcnow() yesterday = now - datetime.timedelta(days=1) yesterday_noon = datetime.datetime( yesterday.year, yesterday.month, yesterday.day, 12, 0, 0, 0, datetime.timezone.utc, ) def get_sleep_summary() -> SleepGetSummaryResponse: return self._api.sleep_get_summary(lastupdate=yesterday_noon) response = await self._hass.async_add_executor_job(get_sleep_summary) # Set the default to empty lists. raw_values: Dict[GetSleepSummaryField, List[int]] = { field: [] for field in GetSleepSummaryField } # Collect the raw data. for serie in response.series: data = serie.data for field in GetSleepSummaryField: raw_values[field].append(data._asdict()[field.value]) values: Dict[GetSleepSummaryField, float] = {} def average(data: List[int]) -> float: return sum(data) / len(data) def set_value(field: GetSleepSummaryField, func: Callable) -> None: non_nones = [ value for value in raw_values.get(field, []) if value is not None ] values[field] = func(non_nones) if non_nones else None set_value(GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY, average) set_value(GetSleepSummaryField.DEEP_SLEEP_DURATION, sum) set_value(GetSleepSummaryField.DURATION_TO_SLEEP, average) set_value(GetSleepSummaryField.DURATION_TO_WAKEUP, average) set_value(GetSleepSummaryField.HR_AVERAGE, average) set_value(GetSleepSummaryField.HR_MAX, average) set_value(GetSleepSummaryField.HR_MIN, average) set_value(GetSleepSummaryField.LIGHT_SLEEP_DURATION, sum) set_value(GetSleepSummaryField.REM_SLEEP_DURATION, sum) set_value(GetSleepSummaryField.RR_AVERAGE, average) set_value(GetSleepSummaryField.RR_MAX, average) set_value(GetSleepSummaryField.RR_MIN, average) set_value(GetSleepSummaryField.SLEEP_SCORE, max) set_value(GetSleepSummaryField.SNORING, average) set_value(GetSleepSummaryField.SNORING_EPISODE_COUNT, sum) set_value(GetSleepSummaryField.WAKEUP_COUNT, sum) set_value(GetSleepSummaryField.WAKEUP_DURATION, average) return { WITHINGS_MEASURE_TYPE_MAP[field].measurement: round(value, 4) if value is not None else None for field, value in values.items() } async def async_webhook_data_updated(self, data_category: NotifyAppli) -> None: """Handle scenario when data is updated from a webook.""" _LOGGER.debug("Withings webhook triggered") if data_category in { NotifyAppli.WEIGHT, NotifyAppli.CIRCULATORY, NotifyAppli.SLEEP, }: await self.poll_data_update_coordinator.async_request_refresh() elif data_category in {NotifyAppli.BED_IN, NotifyAppli.BED_OUT}: self.webhook_update_coordinator.update_data( Measurement.IN_BED, data_category == NotifyAppli.BED_IN)
async def async_setup_gateway_entry(hass: core.HomeAssistant, entry: config_entries.ConfigEntry): """Set up the Xiaomi Gateway component from a config entry.""" host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] name = entry.title gateway_id = entry.unique_id # For backwards compat if entry.unique_id.endswith("-gateway"): hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) entry.async_on_unload(entry.add_update_listener(update_listener)) # Connect to gateway gateway = ConnectXiaomiGateway(hass, entry) if not await gateway.async_connect_gateway(host, token): return False gateway_info = gateway.gateway_info gateway_model = f"{gateway_info.model}-{gateway_info.hardware_version}" device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, gateway_info.mac_address)}, identifiers={(DOMAIN, gateway_id)}, manufacturer="Xiaomi", name=name, model=gateway_model, sw_version=gateway_info.firmware_version, ) def update_data(): """Fetch data from the subdevice.""" data = {} for sub_device in gateway.gateway_device.devices.values(): try: sub_device.update() except GatewayException as ex: _LOGGER.error("Got exception while fetching the state: %s", ex) data[sub_device.sid] = {ATTR_AVAILABLE: False} else: data[sub_device.sid] = {ATTR_AVAILABLE: True} return data async def async_update_data(): """Fetch data from the subdevice using async_add_executor_job.""" return await hass.async_add_executor_job(update_data) # Create update coordinator coordinator = DataUpdateCoordinator( hass, _LOGGER, name=name, update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. update_interval=timedelta(seconds=10), ) hass.data[DOMAIN][entry.entry_id] = { CONF_GATEWAY: gateway.gateway_device, KEY_COORDINATOR: coordinator, } for platform in GATEWAY_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform)) return True
async def async_setup_entry(hass, entry): """ Setup from config entries """ hass.data.setdefault(DOMAIN, {}) title = entry.title if UPDATE_INTERVAL not in entry.options: _LOGGER.info( "Set 60 seconds as update_interval as default. Adjust in options for integration" ) hass.config_entries.async_update_entry(entry, options={UPDATE_INTERVAL: 60}) websession = async_get_clientsession(hass) api = SectorAlarmHub(entry.data[CONF_LOCK], entry.data[CONF_TEMP], entry.data[CONF_USERID], entry.data[CONF_PASSWORD], entry.options.get(UPDATE_INTERVAL, 60), websession=websession) async def async_update_data(): """ Fetch data """ now = datetime.utcnow() hass.data[DOMAIN][entry.entry_id]["last_updated"] = now _LOGGER.debug(f"UPDATE_INTERVAL = {entry.options[UPDATE_INTERVAL]}") _LOGGER.debug("last updated = %s", hass.data[DOMAIN][entry.entry_id]["last_updated"]) await api.fetch_info() return True coordinator = DataUpdateCoordinator( hass, _LOGGER, name="sector_api", update_method=async_update_data, update_interval=timedelta(seconds=entry.options[UPDATE_INTERVAL]), ) hass.data[DOMAIN][entry.entry_id] = { "api": api, "coordinator": coordinator, "last_updated": datetime.utcnow() - timedelta(hours=2), "data_listener": [entry.add_update_listener(update_listener)], } _LOGGER.debug("Connected to Sector Alarm API") await coordinator.async_refresh() if not coordinator.last_update_success: raise ConfigEntryNotReady panel_data = await api.get_panel() if panel_data is None: _LOGGER.error("Platform not ready") raise ConfigEntryNotReady else: hass.async_create_task( hass.config_entries.async_forward_entry_setup( entry, "alarm_control_panel")) temp_data = await api.get_thermometers() if temp_data is None or entry.data[CONF_TEMP] == False: _LOGGER.debug("Temp not configured or Temp sensors not found") else: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "sensor")) lock_data = await api.get_locks() if lock_data is None or entry.data[CONF_LOCK] == False: _LOGGER.debug("Lock not configured or door lock not found") else: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "lock")) device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, "sa_hub_" + str(api.alarm_id))}, manufacturer="Sector Alarm", name="Sector Hub", model="Hub", sw_version="master", ) return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AirVisual as config entry.""" if CONF_API_KEY in entry.data: _standardize_geography_config_entry(hass, entry) websession = aiohttp_client.async_get_clientsession(hass) cloud_api = CloudAPI(entry.data[CONF_API_KEY], session=websession) async def async_update_data() -> dict[str, Any]: """Get new data from the API.""" if CONF_CITY in entry.data: api_coro = cloud_api.air_quality.city( entry.data[CONF_CITY], entry.data[CONF_STATE], entry.data[CONF_COUNTRY], ) else: api_coro = cloud_api.air_quality.nearest_city( entry.data[CONF_LATITUDE], entry.data[CONF_LONGITUDE], ) try: data = await api_coro return cast(dict[str, Any], data) except (InvalidKeyError, KeyExpiredError) as ex: raise ConfigEntryAuthFailed from ex except AirVisualError as err: raise UpdateFailed( f"Error while retrieving data: {err}") from err coordinator = DataUpdateCoordinator( hass, LOGGER, name=async_get_geography_id(entry.data), # We give a placeholder update interval in order to create the coordinator; # then, below, we use the coordinator's presence (along with any other # coordinators using the same API key) to calculate an actual, leveled # update interval: update_interval=timedelta(minutes=5), update_method=async_update_data, ) # Only geography-based entries have options: entry.async_on_unload(entry.add_update_listener(async_reload_entry)) else: # Remove outdated air_quality entities from the entity registry if they exist: ent_reg = entity_registry.async_get(hass) for entity_entry in [ e for e in ent_reg.entities.values() if e.config_entry_id == entry.entry_id and e.entity_id.startswith("air_quality") ]: LOGGER.debug('Removing deprecated air_quality entity: "%s"', entity_entry.entity_id) ent_reg.async_remove(entity_entry.entity_id) _standardize_node_pro_config_entry(hass, entry) async def async_update_data() -> dict[str, Any]: """Get new data from the API.""" try: async with NodeSamba(entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD]) as node: data = await node.async_get_latest_measurements() return cast(dict[str, Any], data) except NodeProError as err: raise UpdateFailed( f"Error while retrieving data: {err}") from err coordinator = DataUpdateCoordinator( hass, LOGGER, name="Node/Pro data", update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL, update_method=async_update_data, ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator} # Reassess the interval between 2 server requests if CONF_API_KEY in entry.data: async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY]) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True
async def async_setup_entry(hass, entry, async_add_entities): """Set up the binary_sensor platform.""" hub = hass.data[DOMAIN][entry.entry_id] station_name = entry.data[CONF_STATION]["name"] station = entry.data[CONF_STATION] def get_elevator_entities_from_station_information( station_name, station_information ): """Convert station information into a list of elevators.""" elevators = {} if station_information is None: return {} for partial_station in station_information.get("partialStations", []): for elevator in partial_station.get("elevators", []): state = elevator.get("state") != "READY" available = elevator.get("state") != "UNKNOWN" label = elevator.get("label") description = elevator.get("description") if label is not None: name = f"Elevator {label} at {station_name}" else: name = f"Unknown elevator at {station_name}" if description is not None: name += f" ({description})" lines = elevator.get("lines") idx = f"{station_name}-{label}-{lines}" elevators[idx] = { "state": state, "name": name, "available": available, "attributes": { "cabin_width": elevator.get("cabinWidth"), "cabin_length": elevator.get("cabinLength"), "door_width": elevator.get("doorWidth"), "elevator_type": elevator.get("elevatorType"), "button_type": elevator.get("buttonType"), "cause": elevator.get("cause"), "lines": lines, ATTR_ATTRIBUTION: ATTRIBUTION, }, } return elevators async def async_update_data(): """Fetch data from API endpoint. This is the place to pre-process the data to lookup tables so entities can quickly look up their data. """ payload = {"station": station} try: async with async_timeout.timeout(10): return get_elevator_entities_from_station_information( station_name, await hub.gti.stationInformation(payload) ) except InvalidAuth as err: raise UpdateFailed(f"Authentication failed: {err}") from err except ClientConnectorError as err: raise UpdateFailed(f"Network not available: {err}") from err except Exception as err: raise UpdateFailed(f"Error occurred while fetching data: {err}") from err coordinator = DataUpdateCoordinator( hass, _LOGGER, # Name of the data. For logging purposes. name="hvv_departures.binary_sensor", update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. update_interval=timedelta(hours=1), ) # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() async_add_entities( HvvDepartureBinarySensor(coordinator, idx, entry) for (idx, ent) in coordinator.data.items() )
async def async_setup(hass: core.HomeAssistant, config: dict) -> bool: """Set up the Unifi Protect component.""" conf = config[DOMAIN] host = conf.get(CONF_HOST) username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) port = conf.get(CONF_PORT) use_ssl = conf.get(CONF_SSL) minimum_score = conf.get(CONF_MIN_SCORE) scan_interval = conf[CONF_SCAN_INTERVAL] session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) try: upv_server = upv.UpvServer(session, host, port, username, password, use_ssl, minimum_score) _LOGGER.debug("Connected to Unifi Protect Platform") except upv.NotAuthorized: _LOGGER.error("Authorization failure while connecting to NVR") return False except upv.NvrError as ex: _LOGGER.error("NVR refuses to talk to me: %s", str(ex)) raise PlatformNotReady except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to NVR: %s", str(ex)) raise PlatformNotReady coordinator = DataUpdateCoordinator( hass, _LOGGER, name=DOMAIN, update_method=upv_server.update, update_interval=scan_interval, ) # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() hass.data[UPV_DATA] = { "coordinator": coordinator, "upv": upv_server, } async def async_save_thumbnail(call): """Call save video service handler.""" await async_handle_save_thumbnail_service(hass, call) async def async_set_recording_mode(call): """Call Set Recording Mode.""" await async_handle_set_recording_mode(hass, call) async def async_set_ir_mode(call): """Call Set Infrared Mode.""" await async_handle_set_ir_mode(hass, call) hass.services.async_register( DOMAIN, SERVICE_SAVE_THUMBNAIL, async_save_thumbnail, schema=SAVE_THUMBNAIL_SCHEMA, ) hass.services.async_register( DOMAIN, SERVICE_SET_RECORDING_MODE, async_set_recording_mode, schema=SET_RECORDING_MODE_SCHEMA, ) hass.services.async_register( DOMAIN, SERVICE_SET_IR_MODE, async_set_ir_mode, schema=SET_IR_MODE_SCHEMA, ) return True
async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Set up the Xiaomi Gateway component from a config entry.""" host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] name = entry.title gateway_id = entry.unique_id assert gateway_id # For backwards compat if gateway_id.endswith("-gateway"): hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) entry.async_on_unload(entry.add_update_listener(update_listener)) # Connect to gateway gateway = ConnectXiaomiGateway(hass, entry) try: await gateway.async_connect_gateway(host, token) except AuthException as error: raise ConfigEntryAuthFailed() from error except SetupException as error: raise ConfigEntryNotReady() from error gateway_info = gateway.gateway_info device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, gateway_info.mac_address)}, identifiers={(DOMAIN, gateway_id)}, manufacturer="Xiaomi", name=name, model=gateway_info.model, sw_version=gateway_info.firmware_version, hw_version=gateway_info.hardware_version, ) def update_data_factory(sub_device): """Create update function for a subdevice.""" async def async_update_data(): """Fetch data from the subdevice.""" try: await hass.async_add_executor_job(sub_device.update) except GatewayException as ex: _LOGGER.error("Got exception while fetching the state: %s", ex) return {ATTR_AVAILABLE: False} return {ATTR_AVAILABLE: True} return async_update_data coordinator_dict: dict[str, DataUpdateCoordinator] = {} for sub_device in gateway.gateway_device.devices.values(): # Create update coordinator coordinator_dict[sub_device.sid] = DataUpdateCoordinator( hass, _LOGGER, name=name, update_method=update_data_factory(sub_device), # Polling interval. Will only be polled if there are subscribers. update_interval=UPDATE_INTERVAL, ) hass.data[DOMAIN][entry.entry_id] = { CONF_GATEWAY: gateway.gateway_device, KEY_COORDINATOR: coordinator_dict, } await hass.config_entries.async_forward_entry_setups(entry, GATEWAY_PLATFORMS)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up sma from a config entry.""" # Init the SMA interface protocol = "https" if entry.data[CONF_SSL] else "http" url = f"{protocol}://{entry.data[CONF_HOST]}" verify_ssl = entry.data[CONF_VERIFY_SSL] group = entry.data[CONF_GROUP] password = entry.data[CONF_PASSWORD] session = async_get_clientsession(hass, verify_ssl=verify_ssl) sma = pysma.SMA(session, url, password, group) try: # Get updated device info sma_device_info = await sma.device_info() # Get all device sensors sensor_def = await sma.get_sensors() except ( pysma.exceptions.SmaReadException, pysma.exceptions.SmaConnectionException, ) as exc: raise ConfigEntryNotReady from exc if TYPE_CHECKING: assert entry.unique_id # Create DeviceInfo object from sma_device_info device_info = DeviceInfo( configuration_url=url, identifiers={(DOMAIN, entry.unique_id)}, manufacturer=sma_device_info["manufacturer"], model=sma_device_info["type"], name=sma_device_info["name"], sw_version=sma_device_info["sw_version"], ) # Define the coordinator async def async_update_data(): """Update the used SMA sensors.""" try: await sma.read(sensor_def) except ( pysma.exceptions.SmaReadException, pysma.exceptions.SmaConnectionException, ) as exc: raise UpdateFailed(exc) from exc interval = timedelta( seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) coordinator = DataUpdateCoordinator( hass, _LOGGER, name="sma", update_method=async_update_data, update_interval=interval, ) try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: await sma.close_session() raise # Ensure we logout on shutdown async def async_close_session(event): """Close the session.""" await sma.close_session() remove_stop_listener = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_session) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { PYSMA_OBJECT: sma, PYSMA_COORDINATOR: coordinator, PYSMA_SENSORS: sensor_def, PYSMA_REMOVE_LISTENER: remove_stop_listener, PYSMA_DEVICE_INFO: device_info, } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up RainMachine as config entry.""" hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} entry_updates = {} if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: entry_updates["unique_id"] = entry.data[CONF_IP_ADDRESS] if CONF_ZONE_RUN_TIME in entry.data: # If a zone run time exists in the config entry's data, pop it and move it to # options: data = {**entry.data} entry_updates["data"] = data entry_updates["options"] = { **entry.options, CONF_ZONE_RUN_TIME: data.pop(CONF_ZONE_RUN_TIME), } if entry_updates: hass.config_entries.async_update_entry(entry, **entry_updates) _verify_domain_control = verify_domain_control(hass, DOMAIN) websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) try: await client.load_local( entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], port=entry.data[CONF_PORT], ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), ) except RainMachineError as err: LOGGER.error("An error occurred: %s", err) raise ConfigEntryNotReady from err # regenmaschine can load multiple controllers at once, but we only grab the one # we loaded above: controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] = next( iter(client.controllers.values())) async def async_update(api_category: str) -> dict: """Update the appropriate API data based on a category.""" try: if api_category == DATA_PROGRAMS: return await controller.programs.all(include_inactive=True) if api_category == DATA_PROVISION_SETTINGS: return await controller.provisioning.settings() if api_category == DATA_RESTRICTIONS_CURRENT: return await controller.restrictions.current() if api_category == DATA_RESTRICTIONS_UNIVERSAL: return await controller.restrictions.universal() return await controller.zones.all(details=True, include_inactive=True) except RainMachineError as err: raise UpdateFailed(err) from err controller_init_tasks = [] for api_category in [ DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, ]: coordinator = hass.data[DOMAIN][DATA_COORDINATOR][ entry.entry_id][api_category] = DataUpdateCoordinator( hass, LOGGER, name=f'{controller.name} ("{api_category}")', update_interval=DEFAULT_UPDATE_INTERVAL, update_method=partial(async_update, api_category), ) controller_init_tasks.append(coordinator.async_refresh()) await asyncio.gather(*controller_init_tasks) for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component)) @_verify_domain_control async def disable_program(call: ServiceCall): """Disable a program.""" await controller.programs.disable(call.data[CONF_PROGRAM_ID]) await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def disable_zone(call: ServiceCall): """Disable a zone.""" await controller.zones.disable(call.data[CONF_ZONE_ID]) await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def enable_program(call: ServiceCall): """Enable a program.""" await controller.programs.enable(call.data[CONF_PROGRAM_ID]) await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def enable_zone(call: ServiceCall): """Enable a zone.""" await controller.zones.enable(call.data[CONF_ZONE_ID]) await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def pause_watering(call: ServiceCall): """Pause watering for a set number of seconds.""" await controller.watering.pause_all(call.data[CONF_SECONDS]) await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def start_program(call: ServiceCall): """Start a particular program.""" await controller.programs.start(call.data[CONF_PROGRAM_ID]) await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def start_zone(call: ServiceCall): """Start a particular zone for a certain amount of time.""" await controller.zones.start(call.data[CONF_ZONE_ID], call.data[CONF_ZONE_RUN_TIME]) await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def stop_all(call: ServiceCall): """Stop all watering.""" await controller.watering.stop_all() await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def stop_program(call: ServiceCall): """Stop a program.""" await controller.programs.stop(call.data[CONF_PROGRAM_ID]) await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def stop_zone(call: ServiceCall): """Stop a zone.""" await controller.zones.stop(call.data[CONF_ZONE_ID]) await async_update_programs_and_zones(hass, entry) @_verify_domain_control async def unpause_watering(call: ServiceCall): """Unpause watering.""" await controller.watering.unpause_all() await async_update_programs_and_zones(hass, entry) for service, method, schema in [ ("disable_program", disable_program, SERVICE_ALTER_PROGRAM), ("disable_zone", disable_zone, SERVICE_ALTER_ZONE), ("enable_program", enable_program, SERVICE_ALTER_PROGRAM), ("enable_zone", enable_zone, SERVICE_ALTER_ZONE), ("pause_watering", pause_watering, SERVICE_PAUSE_WATERING), ("start_program", start_program, SERVICE_START_PROGRAM_SCHEMA), ("start_zone", start_zone, SERVICE_START_ZONE_SCHEMA), ("stop_all", stop_all, {}), ("stop_program", stop_program, SERVICE_STOP_PROGRAM_SCHEMA), ("stop_zone", stop_zone, SERVICE_STOP_ZONE_SCHEMA), ("unpause_watering", unpause_watering, {}), ]: hass.services.async_register(DOMAIN, service, method, schema=schema) hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener( async_reload_entry) return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plugwise Smiles from a config entry.""" websession = async_get_clientsession(hass, verify_ssl=False) api = Smile( host=entry.data[CONF_HOST], password=entry.data[CONF_PASSWORD], port=entry.data.get(CONF_PORT, DEFAULT_PORT), timeout=30, websession=websession, ) try: connected = await api.connect() if not connected: _LOGGER.error("Unable to connect to Smile") raise ConfigEntryNotReady except Smile.InvalidAuthentication: _LOGGER.error("Invalid Smile ID") return False except Smile.PlugwiseError as err: _LOGGER.error("Error while communicating to device") raise ConfigEntryNotReady from err except asyncio.TimeoutError as err: _LOGGER.error("Timeout while connecting to Smile") raise ConfigEntryNotReady from err update_interval = timedelta(seconds=entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL[api.smile_type])) async def async_update_data(): """Update data via API endpoint.""" try: async with async_timeout.timeout(10): await api.full_update_device() return True except Smile.XMLDataMissingError as err: raise UpdateFailed("Smile update failed") from err coordinator = DataUpdateCoordinator( hass, _LOGGER, name="Smile", update_method=async_update_data, update_interval=update_interval, ) await coordinator.async_refresh() if not coordinator.last_update_success: raise ConfigEntryNotReady api.get_all_devices() if entry.unique_id is None: if api.smile_version[0] != "1.8.0": hass.config_entries.async_update_entry( entry, unique_id=api.smile_hostname) undo_listener = entry.add_update_listener(_update_listener) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { "api": api, COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: undo_listener, } device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, api.gateway_id)}, manufacturer="Plugwise", name=entry.title, model=f"Smile {api.smile_name}", sw_version=api.smile_version[0], ) single_master_thermostat = api.single_master_thermostat() platforms = ALL_PLATFORMS if single_master_thermostat is None: platforms = SENSOR_PLATFORMS for component in platforms: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component)) return True
class SensorManager: """Class that handles registering and updating Hue sensor entities. Intended to be a singleton. """ SCAN_INTERVAL = timedelta(seconds=5) def __init__(self, bridge): """Initialize the sensor manager.""" self.bridge = bridge self._component_add_entities = {} self.current = {} self.current_events = {} self._enabled_platforms = ("binary_sensor", "sensor") self.coordinator = DataUpdateCoordinator( bridge.hass, _LOGGER, name="sensor", update_method=self.async_update_data, update_interval=self.SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True), ) async def async_update_data(self): """Update sensor data.""" try: with async_timeout.timeout(4): return await self.bridge.async_request_call( self.bridge.api.sensors.update) except Unauthorized as err: await self.bridge.handle_unauthorized_error() raise UpdateFailed("Unauthorized") from err except AiohueException as err: raise UpdateFailed(f"Hue error: {err}") from err async def async_register_component(self, platform, async_add_entities): """Register async_add_entities methods for components.""" self._component_add_entities[platform] = async_add_entities if len(self._component_add_entities) < len(self._enabled_platforms): _LOGGER.debug("Aborting start with %s, waiting for the rest", platform) return # We have all components available, start the updating. self.bridge.reset_jobs.append( self.coordinator.async_add_listener(self.async_update_items)) await self.coordinator.async_refresh() @callback def async_update_items(self): """Update sensors from the bridge.""" api = self.bridge.api.sensors if len(self._component_add_entities) < len(self._enabled_platforms): return to_add = {} primary_sensor_devices = {} current = self.current # Physical Hue motion sensors present as three sensors in the API: a # presence sensor, a temperature sensor, and a light level sensor. Of # these, only the presence sensor is assigned the user-friendly name # that the user has given to the device. Each of these sensors is # linked by a common device_id, which is the first twenty-three # characters of the unique id (then followed by a hyphen and an ID # specific to the individual sensor). # # To set up neat values, and assign the sensor entities to the same # device, we first, iterate over all the sensors and find the Hue # presence sensors, then iterate over all the remaining sensors - # finding the remaining ones that may or may not be related to the # presence sensors. for item_id in api: if api[item_id].type != TYPE_ZLL_PRESENCE: continue primary_sensor_devices[_device_id(api[item_id])] = api[item_id] # Iterate again now we have all the presence sensors, and add the # related sensors with nice names where appropriate. for item_id in api: uniqueid = api[item_id].uniqueid if current.get(uniqueid, self.current_events.get(uniqueid)) is not None: continue sensor_type = api[item_id].type # Check for event generator devices event_config = EVENT_CONFIG_MAP.get(sensor_type) if event_config is not None: base_name = api[item_id].name name = event_config["name_format"].format(base_name) new_event = event_config["class"](api[item_id], name, self.bridge) self.bridge.hass.async_create_task( new_event.async_update_device_registry()) self.current_events[uniqueid] = new_event sensor_config = SENSOR_CONFIG_MAP.get(sensor_type) if sensor_config is None: continue base_name = api[item_id].name primary_sensor = primary_sensor_devices.get( _device_id(api[item_id])) if primary_sensor is not None: base_name = primary_sensor.name name = sensor_config["name_format"].format(base_name) current[uniqueid] = sensor_config["class"]( api[item_id], name, self.bridge, primary_sensor=primary_sensor) to_add.setdefault(sensor_config["platform"], []).append(current[uniqueid]) self.bridge.hass.async_create_task( remove_devices( self.bridge, [value.uniqueid for value in api.values()], current, )) for platform in to_add: self._component_add_entities[platform](to_add[platform])
async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Synology DSM sensors.""" # Migrate old unique_id @callback def _async_migrator( entity_entry: entity_registry.RegistryEntry, ) -> dict[str, str] | None: """Migrate away from ID using label.""" # Reject if new unique_id if "SYNO." in entity_entry.unique_id: return None entries = ( *STORAGE_DISK_BINARY_SENSORS, *STORAGE_DISK_SENSORS, *STORAGE_VOL_SENSORS, *UTILISATION_SENSORS, ) infos = entity_entry.unique_id.split("_") serial = infos.pop(0) label = infos.pop(0) device_id = "_".join(infos) # Removed entity if ("Type" in entity_entry.unique_id or "Device" in entity_entry.unique_id or "Name" in entity_entry.unique_id): return None entity_type: str | None = None for description in entries: if (device_id and description.name == "Status" and "Status" in entity_entry.unique_id and "(Smart)" not in entity_entry.unique_id): if "sd" in device_id and "disk" in description.key: entity_type = description.key continue if "volume" in device_id and "volume" in description.key: entity_type = description.key continue if description.name == label: entity_type = description.key if entity_type is None: return None new_unique_id = "_".join([serial, entity_type]) if device_id: new_unique_id += f"_{device_id}" _LOGGER.info( "Migrating unique_id from [%s] to [%s]", entity_entry.unique_id, new_unique_id, ) return {"new_unique_id": new_unique_id} await entity_registry.async_migrate_entries(hass, entry.entry_id, _async_migrator) # migrate device indetifiers dev_reg = await get_dev_reg(hass) devices: list[ DeviceEntry] = device_registry.async_entries_for_config_entry( dev_reg, entry.entry_id) for device in devices: old_identifier = list(next(iter(device.identifiers))) if len(old_identifier) > 2: new_identifier = {(old_identifier.pop(0), "_".join([str(x) for x in old_identifier]))} _LOGGER.debug("migrate identifier '%s' to '%s'", device.identifiers, new_identifier) dev_reg.async_update_device(device.id, new_identifiers=new_identifier) # Migrate existing entry configuration if entry.data.get(CONF_VERIFY_SSL) is None: hass.config_entries.async_update_entry( entry, data={ **entry.data, CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL }) # Continue setup api = SynoApi(hass, entry) try: await api.async_setup() except ( SynologyDSMLogin2SARequiredException, SynologyDSMLoginDisabledAccountException, SynologyDSMLoginInvalidException, SynologyDSMLoginPermissionDeniedException, ) as err: if err.args[0] and isinstance(err.args[0], dict): # pylint: disable=no-member details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryAuthFailed(f"reason: {details}") from err except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: if err.args[0] and isinstance(err.args[0], dict): # pylint: disable=no-member details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryNotReady(details) from err hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = { UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), SYNO_API: api, SYSTEM_LOADED: True, } # Services await _async_setup_services(hass) # For SSDP compat if not entry.data.get(CONF_MAC): network = await hass.async_add_executor_job(getattr, api.dsm, "network") hass.config_entries.async_update_entry(entry, data={ **entry.data, CONF_MAC: network.macs }) async def async_coordinator_update_data_cameras( ) -> dict[str, dict[str, SynoCamera]] | None: """Fetch all camera data from api.""" if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: raise UpdateFailed("System not fully loaded") if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: return None surveillance_station = api.surveillance_station try: async with async_timeout.timeout(30): await hass.async_add_executor_job(surveillance_station.update) except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err return { "cameras": { camera.id: camera for camera in surveillance_station.get_all_cameras() } } async def async_coordinator_update_data_central() -> None: """Fetch all device and sensor data from api.""" try: await api.async_update() except Exception as err: raise UpdateFailed(f"Error communicating with API: {err}") from err return None async def async_coordinator_update_data_switches( ) -> dict[str, dict[str, Any]] | None: """Fetch all switch data from api.""" if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: raise UpdateFailed("System not fully loaded") if SynoSurveillanceStation.HOME_MODE_API_KEY not in api.dsm.apis: return None surveillance_station = api.surveillance_station return { "switches": { "home_mode": await hass.async_add_executor_job( surveillance_station.get_home_mode_status) } } hass.data[DOMAIN][ entry.unique_id][COORDINATOR_CAMERAS] = DataUpdateCoordinator( hass, _LOGGER, name=f"{entry.unique_id}_cameras", update_method=async_coordinator_update_data_cameras, update_interval=timedelta(seconds=30), ) hass.data[DOMAIN][ entry.unique_id][COORDINATOR_CENTRAL] = DataUpdateCoordinator( hass, _LOGGER, name=f"{entry.unique_id}_central", update_method=async_coordinator_update_data_central, update_interval=timedelta(minutes=entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)), ) hass.data[DOMAIN][ entry.unique_id][COORDINATOR_SWITCHES] = DataUpdateCoordinator( hass, _LOGGER, name=f"{entry.unique_id}_switches", update_method=async_coordinator_update_data_switches, update_interval=timedelta(seconds=30), ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Hunter Douglas PowerView from a config entry.""" config = entry.data hub_address = config.get(CONF_HOST) websession = async_get_clientsession(hass) pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) try: async with async_timeout.timeout(10): device_info = await async_get_device_info(pv_request) async with async_timeout.timeout(10): rooms = Rooms(pv_request) room_data = _async_map_data_by_id( (await rooms.get_resources())[ROOM_DATA]) async with async_timeout.timeout(10): scenes = Scenes(pv_request) scene_data = _async_map_data_by_id( (await scenes.get_resources())[SCENE_DATA]) async with async_timeout.timeout(10): shades = Shades(pv_request) shade_data = _async_map_data_by_id( (await shades.get_resources())[SHADE_DATA]) except HUB_EXCEPTIONS as err: _LOGGER.error("Connection error to PowerView hub: %s", hub_address) raise ConfigEntryNotReady from err if not device_info: _LOGGER.error("Unable to initialize PowerView hub: %s", hub_address) raise ConfigEntryNotReady async def async_update_data(): """Fetch data from shade endpoint.""" async with async_timeout.timeout(10): shade_entries = await shades.get_resources() if not shade_entries: raise UpdateFailed("Failed to fetch new shade data.") return _async_map_data_by_id(shade_entries[SHADE_DATA]) coordinator = DataUpdateCoordinator( hass, _LOGGER, name="powerview hub", update_method=async_update_data, update_interval=timedelta(seconds=60), ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { PV_API: pv_request, PV_ROOM_DATA: room_data, PV_SCENE_DATA: scene_data, PV_SHADES: shades, PV_SHADE_DATA: shade_data, COORDINATOR: coordinator, DEVICE_INFO: device_info, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True
surveillance_station = api.surveillance_station return { "switches": { "home_mode": await hass.async_add_executor_job( surveillance_station.get_home_mode_status) } } hass.data[DOMAIN][ entry.unique_id][COORDINATOR_CAMERAS] = DataUpdateCoordinator( hass, _LOGGER, name=f"{entry.unique_id}_cameras", update_method=async_coordinator_update_data_cameras, update_interval=timedelta(seconds=30), ) hass.data[DOMAIN][ entry.unique_id][COORDINATOR_CENTRAL] = DataUpdateCoordinator( hass, _LOGGER, name=f"{entry.unique_id}_central", update_method=async_coordinator_update_data_central, update_interval=timedelta(minutes=entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)), ) hass.data[DOMAIN][