From 7da4ee7c4bd0dc3ff53ec23377e751bc6a6fa2b5 Mon Sep 17 00:00:00 2001 From: tarzan77cz Date: Fri, 11 Sep 2026 10:51:19 +0200 Subject: [PATCH 1/4] Fix HA deprecation warnings for device registry - Cast hw_version and sw_version to str (API returns int) - Use async_get_device_by_identifier instead of deprecated async_get_device --- custom_components/webastoconnect/__init__.py | 4 +++- custom_components/webastoconnect/base.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/webastoconnect/__init__.py b/custom_components/webastoconnect/__init__.py index fce1a16..9a9b175 100644 --- a/custom_components/webastoconnect/__init__.py +++ b/custom_components/webastoconnect/__init__.py @@ -132,7 +132,9 @@ async def _async_update_device_registry_name( ) -> None: """Update the device registry name.""" device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_device({(DOMAIN, str(device_id))}) + device_entry = device_registry.async_get_device_by_identifier( + DOMAIN, str(device_id) + ) if device_entry is None or device_entry.name_by_user is not None: return if device_entry.name == device_name: diff --git a/custom_components/webastoconnect/base.py b/custom_components/webastoconnect/base.py index cb92e6d..ffc9c81 100644 --- a/custom_components/webastoconnect/base.py +++ b/custom_components/webastoconnect/base.py @@ -130,8 +130,8 @@ def __init__( "name": self._device_name, "model": "ThermoConnect", "manufacturer": "Webasto", - "hw_version": settings.get("hw_version", "Unknown"), - "sw_version": settings.get("sw_version", "Unknown"), + "hw_version": str(settings.get("hw_version") or "Unknown"), + "sw_version": str(settings.get("sw_version") or "Unknown"), "configuration_url": "https://my.webastoconnect.com", } From 15381fbb6f15982c874d7235d4240643741a45c7 Mon Sep 17 00:00:00 2001 From: tarzan77cz Date: Fri, 11 Sep 2026 11:19:57 +0200 Subject: [PATCH 2/4] Fix async_get_device_by_identifier call signature and pass entry_id --- custom_components/webastoconnect/__init__.py | 229 +------------------ 1 file changed, 3 insertions(+), 226 deletions(-) diff --git a/custom_components/webastoconnect/__init__.py b/custom_components/webastoconnect/__init__.py index 9a9b175..ab09bf1 100644 --- a/custom_components/webastoconnect/__init__.py +++ b/custom_components/webastoconnect/__init__.py @@ -127,13 +127,14 @@ def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: async def _async_update_device_registry_name( hass: HomeAssistant, + entry: ConfigEntry, device_id: int, device_name: str, ) -> None: """Update the device registry name.""" device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device_by_identifier( - DOMAIN, str(device_id) + (DOMAIN, str(device_id)), entry.entry_id ) if device_entry is None or device_entry.name_by_user is not None: return @@ -143,228 +144,4 @@ async def _async_update_device_registry_name( device_registry.async_update_device(device_entry.id, name=device_name) -def _device_names_from_webapi_data(data: dict[str, Any]) -> dict[str, str]: - """Return device names from webapi data.""" - names = {} - if data.get("id") and data.get("alias"): - names[str(data["id"])] = str(data["alias"]) - - for device in data.get("account_info", {}).get("devices", []): - if isinstance(device, dict): - device_id = ( - device.get("id") or device.get("device_id") or device.get("dev_id") - ) - device_name = device.get("name") or device.get("alias") - else: - device_id = device[0] if len(device) > 0 else None - device_name = device[1] if len(device) > 1 else None - if device_id and device_name: - names[str(device_id)] = str(device_name) - - return names - - -async def _async_webapi_device_names( - coordinator: WebastoConnectUpdateCoordinator, -) -> dict[str, str]: - """Fetch device names from webapi when available.""" - if not coordinator.cloud.uses_webapi_session: - return {} - - try: - data = await coordinator.cloud._call(Request.GET_DATA_NOPOLL) # noqa: SLF001 - except ( - InvalidRequestException, - ForbiddenException, - InvalidResponseException, - ) as err: - LOGGER.debug("Could not fetch Webasto webapi device names: %s", err) - return {} - - if not isinstance(data, dict): - return {} - - return _device_names_from_webapi_data(data) - - -async def _async_setup( - hass: HomeAssistant, entry: WebastoConfigEntry -) -> WebastoConnectUpdateCoordinator: - """Set up the integration using a config entry.""" - integration = await async_get_integration(hass, DOMAIN) - LOGGER.info( - STARTUP, - integration.version, - ) - LOGGER.debug( - "Checking Webasto Connect Card installation status for /local/webastoconnect/%s", - CARD_FILENAME, - ) - installed, card_version, card_hash = await hass.async_add_executor_job( - ensure_card_installed, - Path(integration.file_path), - Path(hass.config.path("www")), - ) - if installed: - LOGGER.info( - "Installed Webasto Connect Card v%s at /local/webastoconnect/%s", - card_version, - CARD_FILENAME, - ) - elif card_version is None: - LOGGER.warning( - "Webasto Connect Card assets not found in integration package; skipping install" - ) - else: - LOGGER.debug( - "Webasto Connect Card already up to date (v%s) at /local/webastoconnect/%s", - card_version, - CARD_FILENAME, - ) - await _async_ensure_lovelace_card_resource(hass, card_hash) - - coordinator = WebastoConnectUpdateCoordinator(hass, entry, integration.version) - try: - await coordinator.cloud.connect() - LOGGER.debug( - "Connected to Webasto API for %s", - entry.options.get(CONF_EMAIL, entry.data.get(CONF_EMAIL)), - ) - except UnauthorizedException: - await coordinator.cloud.close() - raise ConfigEntryAuthFailed("Invalid email or password specified") from None - except (InvalidRequestException, ForbiddenException) as err: - await coordinator.cloud.close() - raise ConfigEntryError(f"Webasto API rejected setup request: {err}") from err - except InvalidResponseException as err: - await coordinator.cloud.close() - raise ConfigEntryNotReady( - f"Error connecting to the API - try again later: {err}" - ) from err - except TooManyRequestsException as err: - await coordinator.cloud.close() - raise ConfigEntryError( - f"Rate limited - reload the integration later: {err}" - ) from err - - webapi_device_names = await _async_webapi_device_names(coordinator) - coordinator.device_names = webapi_device_names - - pending_devices = [ - device - for device in coordinator.cloud.devices.values() - if getattr(device, "pending_approval", False) - ] - if pending_devices: - devices = ", ".join( - webasto_device_name( - device, - webapi_device_names.get(str(device.device_id)), - ) - for device in pending_devices - ) - ir.async_create_issue( - hass, - DOMAIN, - PENDING_APPROVAL_ISSUE_ID, - data={"entry_id": entry.entry_id}, - is_fixable=True, - is_persistent=True, - severity=ir.IssueSeverity.WARNING, - translation_key="pending_approval", - translation_placeholders={"devices": devices}, - ) - await coordinator.cloud.close() - raise ConfigEntryError( - "Webasto device association is pending approval in the ThermoConnect app" - ) - - ir.async_delete_issue(hass, DOMAIN, PENDING_APPROVAL_ISSUE_ID) - - if coordinator.cloud.devices: - # connect() already hydrated device state, avoid an immediate duplicate update call. - coordinator.async_set_updated_data(None) - else: - await coordinator.async_config_entry_first_refresh() - - for id, device in coordinator.cloud.devices.items(): - device_name = webasto_device_name(device, webapi_device_names.get(str(id))) - LOGGER.debug("Found device: %s", device_name) - await _async_update_device_registry_name(hass, id, device_name) - # Migrate unique IDs - await _async_migrate_unique_ids(hass, id, device_name, entry) - - return coordinator - - -async def _async_ensure_lovelace_card_resource( - hass: HomeAssistant, card_hash: str | None -) -> None: - """Ensure the Webasto Connect card resource exists in Lovelace storage mode.""" - resource_base_url = f"/local/{CARD_WWW_SUBDIR}/{CARD_FILENAME}" - resource_url = ( - f"{resource_base_url}?v={card_hash}" if card_hash else resource_base_url - ) - - if (lovelace_data := hass.data.get(LOVELACE_DATA)) is None: - LOGGER.debug( - "Lovelace not loaded yet; cannot auto-register resource %s", resource_url - ) - return - - if lovelace_data.resource_mode != MODE_STORAGE: - LOGGER.debug( - "Lovelace resource mode is '%s'; skipping auto-registration of %s", - lovelace_data.resource_mode, - resource_url, - ) - return - - resources = lovelace_data.resources - for resource in resources.async_items() or []: - existing_url = resource.get(CONF_URL) or "" - existing_base_url = existing_url.split("?", 1)[0] - if existing_base_url != resource_base_url: - continue - - update_data: dict[str, str] = {} - if resource.get(CONF_URL) != resource_url: - update_data[CONF_URL] = resource_url - if resource.get(CONF_TYPE) != "module": - update_data[CONF_RESOURCE_TYPE_WS] = "module" - - if update_data: - await resources.async_update_item(resource["id"], update_data) - LOGGER.info("Updated Lovelace resource to %s", resource_url) - else: - LOGGER.debug("Lovelace resource already present for %s", resource_url) - return - - await resources.async_create_item( - {CONF_URL: resource_url, CONF_RESOURCE_TYPE_WS: "module"} - ) - LOGGER.info("Created Lovelace resource for %s", resource_url) - - -async def async_unload_entry(hass: HomeAssistant, entry: WebastoConfigEntry) -> bool: - """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - await entry.runtime_data.coordinator.cloud.close() - entry.runtime_data.update_listener() - loaded_entries = [ - config_entry - for config_entry in hass.config_entries.async_entries(DOMAIN) - if config_entry.state is ConfigEntryState.LOADED - and config_entry.entry_id != entry.entry_id - ] - if not loaded_entries: - async_unregister_services(hass) - return unload_ok - - -async def async_reload_entry(hass: HomeAssistant, entry: WebastoConfigEntry) -> None: - """Reload config entry.""" - await async_unload_entry(hass, entry) - await async_setup_entry(hass, entry) +# ... truncated for push - need full file From 773e9ab14975d02ee28b6a4276d8c18ddb768b67 Mon Sep 17 00:00:00 2001 From: tarzan77cz Date: Fri, 11 Sep 2026 11:20:31 +0200 Subject: [PATCH 3/4] Fix async_get_device_by_identifier signature (full files) --- custom_components/webastoconnect/__init__.py | 226 ++++++++++++++++++- 1 file changed, 225 insertions(+), 1 deletion(-) diff --git a/custom_components/webastoconnect/__init__.py b/custom_components/webastoconnect/__init__.py index ab09bf1..62012c1 100644 --- a/custom_components/webastoconnect/__init__.py +++ b/custom_components/webastoconnect/__init__.py @@ -144,4 +144,228 @@ async def _async_update_device_registry_name( device_registry.async_update_device(device_entry.id, name=device_name) -# ... truncated for push - need full file +def _device_names_from_webapi_data(data: dict[str, Any]) -> dict[str, str]: + """Return device names from webapi data.""" + names = {} + if data.get("id") and data.get("alias"): + names[str(data["id"])] = str(data["alias"]) + + for device in data.get("account_info", {}).get("devices", []): + if isinstance(device, dict): + device_id = ( + device.get("id") or device.get("device_id") or device.get("dev_id") + ) + device_name = device.get("name") or device.get("alias") + else: + device_id = device[0] if len(device) > 0 else None + device_name = device[1] if len(device) > 1 else None + if device_id and device_name: + names[str(device_id)] = str(device_name) + + return names + + +async def _async_webapi_device_names( + coordinator: WebastoConnectUpdateCoordinator, +) -> dict[str, str]: + """Fetch device names from webapi when available.""" + if not coordinator.cloud.uses_webapi_session: + return {} + + try: + data = await coordinator.cloud._call(Request.GET_DATA_NOPOLL) # noqa: SLF001 + except ( + InvalidRequestException, + ForbiddenException, + InvalidResponseException, + ) as err: + LOGGER.debug("Could not fetch Webasto webapi device names: %s", err) + return {} + + if not isinstance(data, dict): + return {} + + return _device_names_from_webapi_data(data) + + +async def _async_setup( + hass: HomeAssistant, entry: WebastoConfigEntry +) -> WebastoConnectUpdateCoordinator: + """Set up the integration using a config entry.""" + integration = await async_get_integration(hass, DOMAIN) + LOGGER.info( + STARTUP, + integration.version, + ) + LOGGER.debug( + "Checking Webasto Connect Card installation status for /local/webastoconnect/%s", + CARD_FILENAME, + ) + installed, card_version, card_hash = await hass.async_add_executor_job( + ensure_card_installed, + Path(integration.file_path), + Path(hass.config.path("www")), + ) + if installed: + LOGGER.info( + "Installed Webasto Connect Card v%s at /local/webastoconnect/%s", + card_version, + CARD_FILENAME, + ) + elif card_version is None: + LOGGER.warning( + "Webasto Connect Card assets not found in integration package; skipping install" + ) + else: + LOGGER.debug( + "Webasto Connect Card already up to date (v%s) at /local/webastoconnect/%s", + card_version, + CARD_FILENAME, + ) + await _async_ensure_lovelace_card_resource(hass, card_hash) + + coordinator = WebastoConnectUpdateCoordinator(hass, entry, integration.version) + try: + await coordinator.cloud.connect() + LOGGER.debug( + "Connected to Webasto API for %s", + entry.options.get(CONF_EMAIL, entry.data.get(CONF_EMAIL)), + ) + except UnauthorizedException: + await coordinator.cloud.close() + raise ConfigEntryAuthFailed("Invalid email or password specified") from None + except (InvalidRequestException, ForbiddenException) as err: + await coordinator.cloud.close() + raise ConfigEntryError(f"Webasto API rejected setup request: {err}") from err + except InvalidResponseException as err: + await coordinator.cloud.close() + raise ConfigEntryNotReady( + f"Error connecting to the API - try again later: {err}" + ) from err + except TooManyRequestsException as err: + await coordinator.cloud.close() + raise ConfigEntryError( + f"Rate limited - reload the integration later: {err}" + ) from err + + webapi_device_names = await _async_webapi_device_names(coordinator) + coordinator.device_names = webapi_device_names + + pending_devices = [ + device + for device in coordinator.cloud.devices.values() + if getattr(device, "pending_approval", False) + ] + if pending_devices: + devices = ", ".join( + webasto_device_name( + device, + webapi_device_names.get(str(device.device_id)), + ) + for device in pending_devices + ) + ir.async_create_issue( + hass, + DOMAIN, + PENDING_APPROVAL_ISSUE_ID, + data={"entry_id": entry.entry_id}, + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="pending_approval", + translation_placeholders={"devices": devices}, + ) + await coordinator.cloud.close() + raise ConfigEntryError( + "Webasto device association is pending approval in the ThermoConnect app" + ) + + ir.async_delete_issue(hass, DOMAIN, PENDING_APPROVAL_ISSUE_ID) + + if coordinator.cloud.devices: + # connect() already hydrated device state, avoid an immediate duplicate update call. + coordinator.async_set_updated_data(None) + else: + await coordinator.async_config_entry_first_refresh() + + for id, device in coordinator.cloud.devices.items(): + device_name = webasto_device_name(device, webapi_device_names.get(str(id))) + LOGGER.debug("Found device: %s", device_name) + await _async_update_device_registry_name(hass, entry, id, device_name) + # Migrate unique IDs + await _async_migrate_unique_ids(hass, id, device_name, entry) + + return coordinator + + +async def _async_ensure_lovelace_card_resource( + hass: HomeAssistant, card_hash: str | None +) -> None: + """Ensure the Webasto Connect card resource exists in Lovelace storage mode.""" + resource_base_url = f"/local/{CARD_WWW_SUBDIR}/{CARD_FILENAME}" + resource_url = ( + f"{resource_base_url}?v={card_hash}" if card_hash else resource_base_url + ) + + if (lovelace_data := hass.data.get(LOVELACE_DATA)) is None: + LOGGER.debug( + "Lovelace not loaded yet; cannot auto-register resource %s", resource_url + ) + return + + if lovelace_data.resource_mode != MODE_STORAGE: + LOGGER.debug( + "Lovelace resource mode is '%s'; skipping auto-registration of %s", + lovelace_data.resource_mode, + resource_url, + ) + return + + resources = lovelace_data.resources + for resource in resources.async_items() or []: + existing_url = resource.get(CONF_URL) or "" + existing_base_url = existing_url.split("?", 1)[0] + if existing_base_url != resource_base_url: + continue + + update_data: dict[str, str] = {} + if resource.get(CONF_URL) != resource_url: + update_data[CONF_URL] = resource_url + if resource.get(CONF_TYPE) != "module": + update_data[CONF_RESOURCE_TYPE_WS] = "module" + + if update_data: + await resources.async_update_item(resource["id"], update_data) + LOGGER.info("Updated Lovelace resource to %s", resource_url) + else: + LOGGER.debug("Lovelace resource already present for %s", resource_url) + return + + await resources.async_create_item( + {CONF_URL: resource_url, CONF_RESOURCE_TYPE_WS: "module"} + ) + LOGGER.info("Created Lovelace resource for %s", resource_url) + + +async def async_unload_entry(hass: HomeAssistant, entry: WebastoConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + await entry.runtime_data.coordinator.cloud.close() + entry.runtime_data.update_listener() + loaded_entries = [ + config_entry + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.state is ConfigEntryState.LOADED + and config_entry.entry_id != entry.entry_id + ] + if not loaded_entries: + async_unregister_services(hass) + return unload_ok + + +async def async_reload_entry(hass: HomeAssistant, entry: WebastoConfigEntry) -> None: + """Reload config entry.""" + await async_unload_entry(hass, entry) + await async_setup_entry(hass, entry) From ab024c1b8d55ea919982481f74e309986875b1fe Mon Sep 17 00:00:00 2001 From: tarzan77cz Date: Fri, 11 Sep 2026 11:20:33 +0200 Subject: [PATCH 4/4] Fix async_get_device_by_identifier signature (full files)