class CloudPreferences: """Handle cloud preferences.""" def __init__(self, hass): """Initialize cloud prefs.""" self._hass = hass self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._prefs = None self._listeners = [] self.last_updated: set[str] = set() async def async_initialize(self): """Finish initializing the preferences.""" if (prefs := await self._store.async_load()) is None: prefs = self._empty_config("") self._prefs = prefs if PREF_GOOGLE_LOCAL_WEBHOOK_ID not in self._prefs: await self._save_prefs( { **self._prefs, PREF_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(), } )
def add_agent_user_id(self, agent_user_id): """Add an agent user id to store.""" if agent_user_id not in self._data[STORE_AGENT_USER_IDS]: self._data[STORE_AGENT_USER_IDS][agent_user_id] = { STORE_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(), } self._store.async_delay_save(lambda: self._data, 1.0)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Withings from a config entry.""" config_updates = {} # Add a unique id if it's an older config entry. if entry.unique_id != entry.data["token"]["userid"] or not isinstance( entry.unique_id, str): config_updates["unique_id"] = str(entry.data["token"]["userid"]) # Add the webhook configuration. if CONF_WEBHOOK_ID not in entry.data: webhook_id = webhook.async_generate_id() config_updates["data"] = { **entry.data, **{ const.CONF_USE_WEBHOOK: hass.data[DOMAIN][const.CONFIG][const.CONF_USE_WEBHOOK], CONF_WEBHOOK_ID: webhook_id, const.CONF_WEBHOOK_URL: entry.data.get( const.CONF_WEBHOOK_URL, webhook.async_generate_url(hass, webhook_id), ), }, } if config_updates: hass.config_entries.async_update_entry(entry, **config_updates) data_manager = await async_get_data_manager(hass, entry) _LOGGER.debug("Confirming %s is authenticated to withings", data_manager.profile) await data_manager.poll_data_update_coordinator.async_config_entry_first_refresh( ) webhook.async_register( hass, const.DOMAIN, "Withings notify", data_manager.webhook_config.id, async_webhook_handler, ) # Perform first webhook subscription check. if data_manager.webhook_config.enabled: data_manager.async_start_polling_webhook_subscriptions() @callback def async_call_later_callback(now) -> None: hass.async_create_task( data_manager.subscription_update_coordinator.async_refresh()) # Start subscription check in the background, outside this component's setup. async_call_later(hass, 1, async_call_later_callback) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True
async def test_webhook_local_only(hass, mock_client): """Test posting a webhook with local only.""" hooks = [] webhook_id = webhook.async_generate_id() async def handle(*args): """Handle webhook.""" hooks.append((args[0], args[1], await args[2].text())) webhook.async_register(hass, "test", "Test hook", webhook_id, handle, local_only=True) resp = await mock_client.post(f"/api/webhook/{webhook_id}", json={"data": True}) assert resp.status == HTTPStatus.OK assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id assert hooks[0][2] == '{"data": true}' # Request from remote IP with patch( "homeassistant.components.webhook.ip_address", return_value=ip_address("123.123.123.123"), ): resp = await mock_client.post(f"/api/webhook/{webhook_id}", json={"data": True}) assert resp.status == HTTPStatus.OK # No hook received assert len(hooks) == 1
async def async_get_or_create_registered_webhook_id_and_url(hass, entry): """Generate webhook ID.""" config = entry.data.copy() updated_config = False webhook_url = None if not (webhook_id := config.get(CONF_WEBHOOK_ID)): webhook_id = webhook.async_generate_id() config[CONF_WEBHOOK_ID] = webhook_id updated_config = True
async def _get_webhook_id(self): """Generate webhook ID.""" webhook_id = webhook.async_generate_id() if cloud.async_active_subscription(self.hass): webhook_url = await cloud.async_create_cloudhook( self.hass, webhook_id) cloudhook = True else: webhook_url = webhook.async_generate_url(self.hass, webhook_id) cloudhook = False return webhook_id, webhook_url, cloudhook
def _empty_config(username): """Return an empty config.""" return { PREF_ALEXA_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS, PREF_ALEXA_ENTITY_CONFIGS: {}, PREF_CLOUD_USER: None, PREF_CLOUDHOOKS: {}, PREF_ENABLE_ALEXA: True, PREF_ENABLE_GOOGLE: True, PREF_ENABLE_REMOTE: False, PREF_GOOGLE_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS, PREF_GOOGLE_ENTITY_CONFIGS: {}, PREF_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(), PREF_GOOGLE_SECURE_DEVICES_PIN: None, PREF_USERNAME: username, }
async def test_webhook_head(hass, mock_client): """Test sending a head request to a webhook.""" hooks = [] webhook_id = webhook.async_generate_id() async def handle(*args): """Handle webhook.""" hooks.append(args) webhook.async_register(hass, "test", "Test hook", webhook_id, handle) resp = await mock_client.head(f"/api/webhook/{webhook_id}") assert resp.status == HTTPStatus.OK assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id assert hooks[0][2].method == "HEAD"
async def test_posting_webhook_no_data(hass, mock_client): """Test posting a webhook with no data.""" hooks = [] webhook_id = webhook.async_generate_id() async def handle(*args): """Handle webhook.""" hooks.append(args) webhook.async_register(hass, "test", "Test hook", webhook_id, handle) resp = await mock_client.post(f"/api/webhook/{webhook_id}") assert resp.status == HTTPStatus.OK assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id assert hooks[0][2].method == "POST" assert await hooks[0][2].text() == ""
async def test_posting_webhook_json(hass, mock_client): """Test posting a webhook with JSON data.""" hooks = [] webhook_id = webhook.async_generate_id() async def handle(*args): """Handle webhook.""" hooks.append((args[0], args[1], await args[2].text())) webhook.async_register(hass, "test", "Test hook", webhook_id, handle) resp = await mock_client.post(f"/api/webhook/{webhook_id}", json={"data": True}) assert resp.status == HTTPStatus.OK assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id assert hooks[0][2] == '{"data": true}'
async def test_unregistering_webhook(hass, mock_client): """Test unregistering a webhook.""" hooks = [] webhook_id = webhook.async_generate_id() async def handle(*args): """Handle webhook.""" hooks.append(args) webhook.async_register(hass, "test", "Test hook", webhook_id, handle) resp = await mock_client.post(f"/api/webhook/{webhook_id}") assert resp.status == HTTPStatus.OK assert len(hooks) == 1 webhook.async_unregister(hass, webhook_id) resp = await mock_client.post(f"/api/webhook/{webhook_id}") assert resp.status == HTTPStatus.OK assert len(hooks) == 1
class GoogleConfigStore: """A configuration store for google assistant.""" _STORAGE_VERSION = 1 _STORAGE_KEY = DOMAIN def __init__(self, hass): """Initialize a configuration store.""" self._hass = hass self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) self._data = None async def async_initialize(self): """Finish initializing the ConfigStore.""" should_save_data = False if (data := await self._store.async_load()) is None: # if the store is not found create an empty one # Note that the first request is always a cloud request, # and that will store the correct agent user id to be used for local requests data = { STORE_AGENT_USER_IDS: {}, } should_save_data = True for agent_user_id, agent_user_data in data[STORE_AGENT_USER_IDS].items( ): if STORE_GOOGLE_LOCAL_WEBHOOK_ID not in agent_user_data: data[STORE_AGENT_USER_IDS][agent_user_id] = { **agent_user_data, STORE_GOOGLE_LOCAL_WEBHOOK_ID: webhook.async_generate_id(), } should_save_data = True if should_save_data: await self._store.async_save(data) self._data = data
async def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry, session): """Set up a webhook to handle binary sensor events.""" if CONF_WEBHOOK_ID not in entry.data: webhook_id = webhook.async_generate_id() webhook_url = webhook.async_generate_url(hass, webhook_id) _LOGGER.info("Registering new webhook at: %s", webhook_url) hass.config_entries.async_update_entry( entry, data={ **entry.data, CONF_WEBHOOK_ID: webhook_id, CONF_WEBHOOK_URL: webhook_url, }, ) await session.update_webhook( entry.data[CONF_WEBHOOK_URL], entry.data[CONF_WEBHOOK_ID], ["*"], ) webhook.async_register(hass, DOMAIN, "Point", entry.data[CONF_WEBHOOK_ID], handle_webhook)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up motionEye from a config entry.""" hass.data.setdefault(DOMAIN, {}) client = create_motioneye_client( entry.data[CONF_URL], admin_username=entry.data.get(CONF_ADMIN_USERNAME), admin_password=entry.data.get(CONF_ADMIN_PASSWORD), surveillance_username=entry.data.get(CONF_SURVEILLANCE_USERNAME), surveillance_password=entry.data.get(CONF_SURVEILLANCE_PASSWORD), session=async_get_clientsession(hass), ) try: await client.async_client_login() except MotionEyeClientInvalidAuthError as exc: await client.async_client_close() raise ConfigEntryAuthFailed from exc except MotionEyeClientError as exc: await client.async_client_close() raise ConfigEntryNotReady from exc # Ensure every loaded entry has a registered webhook id. if CONF_WEBHOOK_ID not in entry.data: hass.config_entries.async_update_entry( entry, data={ **entry.data, CONF_WEBHOOK_ID: async_generate_id() }) webhook_register(hass, DOMAIN, "motionEye", entry.data[CONF_WEBHOOK_ID], handle_webhook) @callback async def async_update_data() -> dict[str, Any] | None: try: return await client.async_get_cameras() except MotionEyeClientError as exc: raise UpdateFailed("Error communicating with API") from exc coordinator = DataUpdateCoordinator( hass, _LOGGER, name=DOMAIN, update_method=async_update_data, update_interval=DEFAULT_SCAN_INTERVAL, ) hass.data[DOMAIN][entry.entry_id] = { CONF_CLIENT: client, CONF_COORDINATOR: coordinator, } current_cameras: set[tuple[str, str]] = set() device_registry = dr.async_get(hass) @callback def _async_process_motioneye_cameras() -> None: """Process motionEye camera additions and removals.""" inbound_camera: set[tuple[str, str]] = set() if coordinator.data is None or KEY_CAMERAS not in coordinator.data: return for camera in coordinator.data[KEY_CAMERAS]: if not is_acceptable_camera(camera): return camera_id = camera[KEY_ID] device_identifier = get_motioneye_device_identifier( entry.entry_id, camera_id) inbound_camera.add(device_identifier) if device_identifier in current_cameras: continue current_cameras.add(device_identifier) _add_camera( hass, device_registry, client, entry, camera_id, camera, device_identifier, ) # Ensure every device associated with this config entry is still in the # list of motionEye cameras, otherwise remove the device (and thus # entities). for device_entry in dr.async_entries_for_config_entry( device_registry, entry.entry_id): for identifier in device_entry.identifiers: if identifier in inbound_camera: break else: device_registry.async_remove_device(device_entry.id) async def setup_then_listen() -> None: await asyncio.gather( *(hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS)) entry.async_on_unload( coordinator.async_add_listener(_async_process_motioneye_cameras)) await coordinator.async_refresh() entry.async_on_unload(entry.add_update_listener(_async_entry_updated)) hass.async_create_task(setup_then_listen()) return True