diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ce7cf1ac124..54522e61ec4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 4 + CACHE_VERSION: 6 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.8" diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index 9df0e60850e..c08e2f1c010 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -1,11 +1,11 @@ """Alexa Devices integration.""" -from homeassistant.const import Platform +from homeassistant.const import CONF_COUNTRY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import _LOGGER, COUNTRY_DOMAINS, DOMAIN from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator from .services import async_setup_services @@ -40,6 +40,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo return True +async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: + """Migrate old entry.""" + if entry.version == 1 and entry.minor_version == 0: + _LOGGER.debug( + "Migrating from version %s.%s", entry.version, entry.minor_version + ) + + # Convert country in domain + country = entry.data[CONF_COUNTRY] + domain = COUNTRY_DOMAINS.get(country, country) + + # Save domain and remove country + new_data = entry.data.copy() + new_data.update({"site": f"https://www.amazon.{domain}"}) + + hass.config_entries.async_update_entry( + entry, data=new_data, version=1, minor_version=1 + ) + + _LOGGER.info( + "Migration to version %s.%s successful", entry.version, entry.minor_version + ) + + return True + + async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/alexa_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py index 3e705d73ade..ca00d3e8250 100644 --- a/homeassistant/components/alexa_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -10,16 +10,14 @@ from aioamazondevices.exceptions import ( CannotAuthenticate, CannotConnect, CannotRetrieveData, - WrongCountry, ) import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.selector import CountrySelector from .const import CONF_LOGIN_DATA, DOMAIN @@ -37,7 +35,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, session = aiohttp_client.async_create_clientsession(hass) api = AmazonEchoApi( session, - data[CONF_COUNTRY], data[CONF_USERNAME], data[CONF_PASSWORD], ) @@ -48,6 +45,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Alexa Devices.""" + VERSION = 1 + MINOR_VERSION = 1 + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -62,8 +62,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except CannotRetrieveData: errors["base"] = "cannot_retrieve_data" - except WrongCountry: - errors["base"] = "wrong_country" else: await self.async_set_unique_id(data["customer_info"]["user_id"]) self._abort_if_unique_id_configured() @@ -78,9 +76,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, data_schema=vol.Schema( { - vol.Required( - CONF_COUNTRY, default=self.hass.config.country - ): CountrySelector(), vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_CODE): cv.string, diff --git a/homeassistant/components/alexa_devices/const.py b/homeassistant/components/alexa_devices/const.py index ca0290a10bc..3ade3ad3ecd 100644 --- a/homeassistant/components/alexa_devices/const.py +++ b/homeassistant/components/alexa_devices/const.py @@ -6,3 +6,22 @@ _LOGGER = logging.getLogger(__package__) DOMAIN = "alexa_devices" CONF_LOGIN_DATA = "login_data" + +DEFAULT_DOMAIN = {"domain": "com"} +COUNTRY_DOMAINS = { + "ar": DEFAULT_DOMAIN, + "at": DEFAULT_DOMAIN, + "au": {"domain": "com.au"}, + "be": {"domain": "com.be"}, + "br": DEFAULT_DOMAIN, + "gb": {"domain": "co.uk"}, + "il": DEFAULT_DOMAIN, + "jp": {"domain": "co.jp"}, + "mx": {"domain": "com.mx"}, + "no": DEFAULT_DOMAIN, + "nz": {"domain": "com.au"}, + "pl": DEFAULT_DOMAIN, + "tr": {"domain": "com.tr"}, + "us": DEFAULT_DOMAIN, + "za": {"domain": "co.za"}, +} diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index f4a1faa4f81..ac033a487ee 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -11,7 +11,7 @@ from aioamazondevices.exceptions import ( from aiohttp import ClientSession from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -44,7 +44,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): ) self.api = AmazonEchoApi( session, - entry.data[CONF_COUNTRY], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.data[CONF_LOGIN_DATA], diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 90410412dfa..cba3af83f44 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "silver", - "requirements": ["aioamazondevices==4.0.0"] + "requirements": ["aioamazondevices==5.0.0"] } diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index 1b1150d5649..720b357d275 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -1,7 +1,6 @@ { "common": { "data_code": "One-time password (OTP code)", - "data_description_country": "The country where your Amazon account is registered.", "data_description_username": "The email address of your Amazon account.", "data_description_password": "The password of your Amazon account.", "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.", @@ -12,13 +11,11 @@ "step": { "user": { "data": { - "country": "[%key:common::config_flow::data::country%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "code": "[%key:component::alexa_devices::common::data_code%]" }, "data_description": { - "country": "[%key:component::alexa_devices::common::data_description_country%]", "username": "[%key:component::alexa_devices::common::data_description_username%]", "password": "[%key:component::alexa_devices::common::data_description_password%]", "code": "[%key:component::alexa_devices::common::data_description_code%]" @@ -46,7 +43,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.", "unknown": "[%key:common::config_flow::error::unknown%]" } }, diff --git a/homeassistant/components/amberelectric/config_flow.py b/homeassistant/components/amberelectric/config_flow.py index c25258e2e33..b5f034b4448 100644 --- a/homeassistant/components/amberelectric/config_flow.py +++ b/homeassistant/components/amberelectric/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.helpers.selector import ( SelectSelectorMode, ) -from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN +from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT API_URL = "https://app.amber.com.au/developers" @@ -64,7 +64,9 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN): api = amberelectric.AmberApi(api_client) try: - sites: list[Site] = filter_sites(api.get_sites()) + sites: list[Site] = filter_sites( + api.get_sites(_request_timeout=REQUEST_TIMEOUT) + ) except amberelectric.ApiException as api_exception: if api_exception.status == 403: self._errors[CONF_API_TOKEN] = "invalid_api_token" diff --git a/homeassistant/components/amberelectric/const.py b/homeassistant/components/amberelectric/const.py index bdb9aa3186c..814b8a9bd6a 100644 --- a/homeassistant/components/amberelectric/const.py +++ b/homeassistant/components/amberelectric/const.py @@ -22,3 +22,5 @@ SERVICE_GET_FORECASTS = "get_forecasts" GENERAL_CHANNEL = "general" CONTROLLED_LOAD_CHANNEL = "controlled_load" FEED_IN_CHANNEL = "feed_in" + +REQUEST_TIMEOUT = 15 diff --git a/homeassistant/components/amberelectric/coordinator.py b/homeassistant/components/amberelectric/coordinator.py index a1efef26aae..2ea14b5200b 100644 --- a/homeassistant/components/amberelectric/coordinator.py +++ b/homeassistant/components/amberelectric/coordinator.py @@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import LOGGER +from .const import LOGGER, REQUEST_TIMEOUT from .helpers import normalize_descriptor type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator] @@ -82,7 +82,11 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): "grid": {}, } try: - data = self._api.get_current_prices(self.site_id, next=288) + data = self._api.get_current_prices( + self.site_id, + next=288, + _request_timeout=REQUEST_TIMEOUT, + ) intervals = [interval.actual_instance for interval in data] except ApiException as api_exception: raise UpdateFailed("Missing price data, skipping update") from api_exception diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index d0d766862ff..7304b8828e1 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "quality_scale": "internal", "requirements": [ "bleak==1.0.1", - "bleak-retry-connector==4.0.1", + "bleak-retry-connector==4.0.2", "bluetooth-adapters==2.0.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index 06ce45cdb3a..e0e2963c340 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -69,12 +69,7 @@ class SHCEntity(SHCBaseEntity): manufacturer=device.manufacturer, model=device.device_model, name=device.name, - via_device=( - DOMAIN, - device.parent_device_id - if device.parent_device_id is not None - else parent_id, - ), + via_device=(DOMAIN, device.root_device_id), ) super().__init__(device=device, parent_id=parent_id, entry_id=entry_id) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index 0c99324efbb..bd2e127df3f 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/bosch_shc", "iot_class": "local_push", "loggers": ["boschshcpy"], - "requirements": ["boschshcpy==0.2.91"], + "requirements": ["boschshcpy==0.2.107"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py index 2c9bee32734..385c88d6eb9 100644 --- a/homeassistant/components/esphome/const.py +++ b/homeassistant/components/esphome/const.py @@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False DEFAULT_PORT: Final = 6053 -STABLE_BLE_VERSION_STR = "2025.5.0" +STABLE_BLE_VERSION_STR = "2025.8.0" STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) PROJECT_URLS = { "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2f2a8e93b1e..ff50567257a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -50,7 +50,7 @@ CONF_EXTRA_JS_URL_ES5 = "extra_js_url_es5" CONF_FRONTEND_REPO = "development_repo" CONF_JS_VERSION = "javascript_version" -DEFAULT_THEME_COLOR = "#03A9F4" +DEFAULT_THEME_COLOR = "#2980b9" DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels") diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 3488ddc5e5c..9fc80cf0e8a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250811.0"] + "requirements": ["home-assistant-frontend==20250811.1"] } diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 35f7f48481e..b037973041b 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -61,18 +61,19 @@ PLACEHOLDER_KEY_REASON = "reason" UNSUPPORTED_REASONS = { "apparmor", + "cgroup_version", "connectivity_check", "content_trust", "dbus", "dns_server", "docker_configuration", "docker_version", - "cgroup_version", "job_conditions", "lxc", "network_manager", "os", "os_agent", + "os_version", "restart_policy", "software", "source_mods", @@ -80,6 +81,7 @@ UNSUPPORTED_REASONS = { "systemd", "systemd_journal", "systemd_resolved", + "virtualization_image", } # Some unsupported reasons also mark the system as unhealthy. If the unsupported reason # provides no additional information beyond the unhealthy one then skip that repair. diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index dde50da1af3..5ea0d217f14 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.78", "babel==2.15.0"] + "requirements": ["holidays==0.79", "babel==2.15.0"] } diff --git a/homeassistant/components/icloud/services.py b/homeassistant/components/icloud/services.py index dbb843e8216..44a2e5d52f7 100644 --- a/homeassistant/components/icloud/services.py +++ b/homeassistant/components/icloud/services.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv from homeassistant.util import slugify -from .account import IcloudAccount +from .account import IcloudAccount, IcloudConfigEntry from .const import ( ATTR_ACCOUNT, ATTR_DEVICE_NAME, @@ -92,8 +92,10 @@ def lost_device(service: ServiceCall) -> None: def update_account(service: ServiceCall) -> None: """Call the update function of an iCloud account.""" if (account := service.data.get(ATTR_ACCOUNT)) is None: - for account in service.hass.data[DOMAIN].values(): - account.keep_alive() + # Update all accounts when no specific account is provided + entry: IcloudConfigEntry + for entry in service.hass.config_entries.async_loaded_entries(DOMAIN): + entry.runtime_data.keep_alive() else: _get_account(service.hass, account).keep_alive() @@ -102,17 +104,12 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount: if account_identifier is None: return None - icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) - if icloud_account is None: - for account in hass.data[DOMAIN].values(): - if account.username == account_identifier: - icloud_account = account + entry: IcloudConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + if entry.runtime_data.username == account_identifier: + return entry.runtime_data - if icloud_account is None: - raise ValueError( - f"No iCloud account with username or name {account_identifier}" - ) - return icloud_account + raise ValueError(f"No iCloud account with username or name {account_identifier}") @callback diff --git a/homeassistant/components/imeon_inverter/coordinator.py b/homeassistant/components/imeon_inverter/coordinator.py index f1963a45579..02e81927005 100644 --- a/homeassistant/components/imeon_inverter/coordinator.py +++ b/homeassistant/components/imeon_inverter/coordinator.py @@ -75,13 +75,11 @@ class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]): data: dict[str, str | float | int] = {} async with timeout(TIMEOUT): - await self._api.login( - self.config_entry.data[CONF_USERNAME], - self.config_entry.data[CONF_PASSWORD], - ) - - # Fetch data using distant API try: + await self._api.login( + self.config_entry.data[CONF_USERNAME], + self.config_entry.data[CONF_PASSWORD], + ) await self._api.update() except (ValueError, ClientError) as e: raise UpdateFailed(e) from e diff --git a/homeassistant/components/imeon_inverter/sensor.py b/homeassistant/components/imeon_inverter/sensor.py index 32d40923fa1..119677c0a8a 100644 --- a/homeassistant/components/imeon_inverter/sensor.py +++ b/homeassistant/components/imeon_inverter/sensor.py @@ -14,7 +14,6 @@ from homeassistant.const import ( EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, - UnitOfEnergy, UnitOfFrequency, UnitOfPower, UnitOfTemperature, @@ -50,8 +49,8 @@ SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key="battery_stored", translation_key="battery_stored", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY_STORAGE, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), # Grid @@ -238,16 +237,16 @@ SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key="pv_consumed", translation_key="pv_consumed", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="pv_injected", translation_key="pv_injected", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="pv_power_1", @@ -290,14 +289,14 @@ SENSOR_DESCRIPTIONS = ( key="monitoring_self_consumption", translation_key="monitoring_self_consumption", native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, suggested_display_precision=2, ), SensorEntityDescription( key="monitoring_self_sufficiency", translation_key="monitoring_self_sufficiency", native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, suggested_display_precision=2, ), # Monitoring (instant minute data) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 145690487d7..b0779b35f14 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["imgw_pib==1.5.3"] + "requirements": ["imgw_pib==1.5.4"] } diff --git a/homeassistant/components/lg_thinq/coordinator.py b/homeassistant/components/lg_thinq/coordinator.py index 9f84c422277..ffdde3188db 100644 --- a/homeassistant/components/lg_thinq/coordinator.py +++ b/homeassistant/components/lg_thinq/coordinator.py @@ -37,7 +37,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): name=f"{DOMAIN}_{ha_bridge.device.device_id}", ) - self.data = {} + self.data = ha_bridge.update_status(None) self.api = ha_bridge self.device_id = ha_bridge.device.device_id self.sub_id = ha_bridge.sub_id diff --git a/homeassistant/components/matter/valve.py b/homeassistant/components/matter/valve.py index bea11468c6b..4cedec74bf2 100644 --- a/homeassistant/components/matter/valve.py +++ b/homeassistant/components/matter/valve.py @@ -52,9 +52,12 @@ class MatterValve(MatterEntity, ValveEntity): async def async_set_valve_position(self, position: int) -> None: """Move the valve to a specific position.""" - await self.send_device_command( - ValveConfigurationAndControl.Commands.Open(targetLevel=position) - ) + if position > 0: + await self.send_device_command( + ValveConfigurationAndControl.Commands.Open(targetLevel=position) + ) + return + await self.send_device_command(ValveConfigurationAndControl.Commands.Close()) @callback def _update_from_device(self) -> None: diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index db622d21f1a..477e77022de 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.07.21"], + "requirements": ["yt-dlp[default]==2025.08.11"], "single_config_entry": true } diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index c5a981dbf46..fa033700043 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/mystrom", "iot_class": "local_polling", "loggers": ["pymystrom"], - "requirements": ["python-mystrom==2.4.0"] + "requirements": ["python-mystrom==2.5.0"] } diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index 253387c254a..d62168a4ad3 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -10,7 +10,12 @@ from typing import Any, Final, cast from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_USER, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.json import save_json @@ -200,7 +205,9 @@ class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="unknown") name = self.nanoleaf.name - await self.async_set_unique_id(name) + await self.async_set_unique_id( + name, raise_on_progress=self.source != SOURCE_USER + ) self._abort_if_unique_id_configured({CONF_HOST: self.nanoleaf.host}) if discovery_integration_import: diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 595c57b1b4b..aeb4ffa0c55 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==9.2.1"] + "requirements": ["pyatmo==9.2.3"] } diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 787040d5691..7ebe5256010 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==4.0.3", "WSDiscovery==2.1.2"] + "requirements": ["onvif-zeep-async==4.0.4", "WSDiscovery==2.1.2"] } diff --git a/homeassistant/components/openai_conversation/entity.py b/homeassistant/components/openai_conversation/entity.py index 748c0c8f874..885832bb4ca 100644 --- a/homeassistant/components/openai_conversation/entity.py +++ b/homeassistant/components/openai_conversation/entity.py @@ -91,6 +91,8 @@ MAX_TOOL_ITERATIONS = 10 def _adjust_schema(schema: dict[str, Any]) -> None: """Adjust the schema to be compatible with OpenAI API.""" if schema["type"] == "object": + schema.setdefault("strict", True) + schema.setdefault("additionalProperties", False) if "properties" not in schema: return @@ -124,8 +126,6 @@ def _format_structured_output( _adjust_schema(result) - result["strict"] = True - result["additionalProperties"] = False return result diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index a10c5b2d15d..e127824ac19 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.15.1"] + "requirements": ["opower==0.15.2"] } diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 2861c52c24a..9fe01c5b952 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "silver", - "requirements": ["renault-api==0.3.1"] + "requirements": ["renault-api==0.4.0"] } diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index aad9b9425aa..efaf8f195ad 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_push", "loggers": ["aiorussound"], "quality_scale": "silver", - "requirements": ["aiorussound==4.8.0"], + "requirements": ["aiorussound==4.8.1"], "zeroconf": ["_rio._tcp.local."] } diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 35354570f23..951d1372a69 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.8"] + "requirements": ["pysmartthings==3.2.9"] } diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 839e419dd96..a857602a584 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -326,7 +326,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" if self._player.volume is not None: - return int(float(self._player.volume)) / 100.0 + return float(self._player.volume) / 100.0 return None @@ -435,7 +435,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - volume_percent = str(int(volume * 100)) + volume_percent = str(round(volume * 100)) await self._player.async_set_volume(volume_percent) await self.coordinator.async_refresh() diff --git a/homeassistant/components/volvo/coordinator.py b/homeassistant/components/volvo/coordinator.py index da23e7875c9..d6c8f349a52 100644 --- a/homeassistant/components/volvo/coordinator.py +++ b/homeassistant/components/volvo/coordinator.py @@ -260,6 +260,8 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator): "Volvo medium interval coordinator", ) + self._supported_capabilities: list[str] = [] + async def _async_determine_api_calls( self, ) -> list[Callable[[], Coroutine[Any, Any, Any]]]: @@ -267,6 +269,31 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator): capabilities = await self.api.async_get_energy_capabilities() if capabilities.get("isSupported", False): - return [self.api.async_get_energy_state] + self._supported_capabilities = [ + key + for key, value in capabilities.items() + if isinstance(value, dict) and value.get("isSupported", False) + ] + + return [self._async_get_energy_state] return [] + + async def _async_get_energy_state( + self, + ) -> dict[str, VolvoCarsValueStatusField | None]: + def _mark_ok( + field: VolvoCarsValueStatusField | None, + ) -> VolvoCarsValueStatusField | None: + if field: + field.status = "OK" + + return field + + energy_state = await self.api.async_get_energy_state() + + return { + key: _mark_ok(value) + for key, value in energy_state.items() + if key in self._supported_capabilities + } diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index a067549f068..bb20d64e17c 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -67,8 +67,8 @@ def _calculate_time_to_service(field: VolvoCarsValue) -> int: def _charging_power_value(field: VolvoCarsValue) -> int: return ( - int(field.value) - if isinstance(field, VolvoCarsValueStatusField) and field.status == "OK" + field.value + if isinstance(field, VolvoCarsValueStatusField) and isinstance(field.value, int) else 0 ) diff --git a/homeassistant/components/withings/diagnostics.py b/homeassistant/components/withings/diagnostics.py index d8b59075368..dd154488be2 100644 --- a/homeassistant/components/withings/diagnostics.py +++ b/homeassistant/components/withings/diagnostics.py @@ -2,16 +2,23 @@ from __future__ import annotations +from dataclasses import asdict from typing import Any from yarl import URL +from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.webhook import async_generate_url as webhook_generate_url from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from . import CONF_CLOUDHOOK_URL, WithingsConfigEntry +TO_REDACT = { + "device_id", + "hashed_device_id", +} + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: WithingsConfigEntry @@ -53,4 +60,8 @@ async def async_get_config_entry_diagnostics( "received_sleep_data": withings_data.sleep_coordinator.data is not None, "received_workout_data": withings_data.workout_coordinator.data is not None, "received_activity_data": withings_data.activity_coordinator.data is not None, + "devices": async_redact_data( + [asdict(v) for v in withings_data.device_coordinator.data.values()], + TO_REDACT, + ), } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index d2309702728..0e336632b2e 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.78"] + "requirements": ["holidays==0.79"] } diff --git a/homeassistant/components/zimi/manifest.json b/homeassistant/components/zimi/manifest.json index 3e019d2f053..58a56c97830 100644 --- a/homeassistant/components/zimi/manifest.json +++ b/homeassistant/components/zimi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/zimi", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["zcc-helper==3.5.2"] + "requirements": ["zcc-helper==3.6"] } diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index b72a71279ab..92912a2cdb5 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -35,6 +35,7 @@ from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import selector from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.usb import UsbServiceInfo @@ -88,6 +89,8 @@ ADDON_USER_INPUT_MAP = { CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY, } +CONF_ADDON_RF_REGION = "rf_region" + EXAMPLE_SERVER_URL = "ws://localhost:3000" ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61") @@ -103,6 +106,19 @@ ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS = ( "#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui" ) +RF_REGIONS = [ + "Australia/New Zealand", + "China", + "Europe", + "Hong Kong", + "India", + "Israel", + "Japan", + "Korea", + "Russia", + "USA", +] + def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: """Return a schema for the manual step.""" @@ -195,10 +211,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self.backup_data: bytes | None = None self.backup_filepath: Path | None = None self.use_addon = False + self._addon_config_updates: dict[str, Any] = {} self._migrating = False self._reconfigure_config_entry: ZwaveJSConfigEntry | None = None self._usb_discovery = False self._recommended_install = False + self._rf_region: str | None = None async def async_step_install_addon( self, user_input: dict[str, Any] | None = None @@ -236,6 +254,21 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Start Z-Wave JS add-on.""" + if self.hass.config.country is None and ( + not self._rf_region or self._rf_region == "Automatic" + ): + # If the country is not set, we need to check the RF region add-on config. + addon_info = await self._async_get_addon_info() + rf_region: str | None = addon_info.options.get(CONF_ADDON_RF_REGION) + self._rf_region = rf_region + if rf_region is None or rf_region == "Automatic": + # If the RF region is not set, we need to ask the user to select it. + return await self.async_step_rf_region() + if config_updates := self._addon_config_updates: + # If we have updates to the add-on config, set them before starting the add-on. + self._addon_config_updates = {} + await self._async_set_addon_config(config_updates) + if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) @@ -629,6 +662,33 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_on_supervisor({CONF_USE_ADDON: True}) return await self.async_step_on_supervisor() + async def async_step_rf_region( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle RF region selection step.""" + if user_input is not None: + # Store the selected RF region + self._addon_config_updates[CONF_ADDON_RF_REGION] = self._rf_region = ( + user_input["rf_region"] + ) + return await self.async_step_start_addon() + + schema = vol.Schema( + { + vol.Required("rf_region"): selector.SelectSelector( + selector.SelectSelectorConfig( + options=RF_REGIONS, + mode=selector.SelectSelectorMode.DROPDOWN, + ) + ), + } + ) + + return self.async_show_form( + step_id="rf_region", + data_schema=schema, + ) + async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -728,7 +788,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } - await self._async_set_addon_config(addon_config_updates) + self._addon_config_updates = addon_config_updates return await self.async_step_start_addon() # Network already exists, go to security keys step @@ -799,7 +859,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } - await self._async_set_addon_config(addon_config_updates) + self._addon_config_updates = addon_config_updates return await self.async_step_start_addon() data_schema = vol.Schema( @@ -1004,7 +1064,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: if self.usb_path: # USB discovery was used, so the device is already known. - await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path}) + self._addon_config_updates[CONF_ADDON_DEVICE] = self.usb_path return await self.async_step_start_addon() # Now that the old controller is gone, we can scan for serial ports again return await self.async_step_choose_serial_port() @@ -1136,6 +1196,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } + addon_config_updates = self._addon_config_updates | addon_config_updates + self._addon_config_updates = {} await self._async_set_addon_config(addon_config_updates) if addon_info.state == AddonState.RUNNING and not self.restart_addon: @@ -1207,7 +1269,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): """Choose a serial port.""" if user_input is not None: self.usb_path = user_input[CONF_USB_PATH] - await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path}) + self._addon_config_updates[CONF_ADDON_DEVICE] = self.usb_path return await self.async_step_start_addon() try: diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 2efb8c8e67c..23b906a9d16 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -4,15 +4,15 @@ from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import Any +from typing import Any, cast import voluptuous as vol -from zwave_js_server.const import CommandClass +from zwave_js_server.const import CommandClass, RssiError from zwave_js_server.const.command_class.meter import ( RESET_METER_OPTION_TARGET_VALUE, RESET_METER_OPTION_TYPE, ) -from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.exceptions import BaseZwaveJSServerError, RssiErrorReceived from zwave_js_server.model.controller import Controller from zwave_js_server.model.controller.statistics import ControllerStatistics from zwave_js_server.model.driver import Driver @@ -1049,7 +1049,7 @@ class ZWaveStatisticsSensor(SensorEntity): self, config_entry: ZwaveJSConfigEntry, driver: Driver, - statistics_src: ZwaveNode | Controller, + statistics_src: Controller | ZwaveNode, description: ZWaveJSStatisticsSensorEntityDescription, ) -> None: """Initialize a Z-Wave statistics entity.""" @@ -1080,13 +1080,31 @@ class ZWaveStatisticsSensor(SensorEntity): ) @callback - def statistics_updated(self, event_data: dict) -> None: + def _statistics_updated(self, event_data: dict) -> None: """Call when statistics updated event is received.""" - self._attr_native_value = self.entity_description.convert( - event_data["statistics_updated"], self.entity_description.key + statistics = cast( + ControllerStatistics | NodeStatistics, event_data["statistics_updated"] ) + self._set_statistics(statistics) self.async_write_ha_state() + @callback + def _set_statistics( + self, statistics: ControllerStatistics | NodeStatistics + ) -> None: + """Set updated statistics.""" + try: + self._attr_native_value = self.entity_description.convert( + statistics, self.entity_description.key + ) + except RssiErrorReceived as err: + if err.error is RssiError.NOT_AVAILABLE: + self._attr_available = False + return + self._attr_native_value = None + # Reset available state. + self._attr_available = True + async def async_added_to_hass(self) -> None: """Call when entity is added.""" self.async_on_remove( @@ -1104,10 +1122,8 @@ class ZWaveStatisticsSensor(SensorEntity): ) ) self.async_on_remove( - self.statistics_src.on("statistics updated", self.statistics_updated) + self.statistics_src.on("statistics updated", self._statistics_updated) ) # Set initial state - self._attr_native_value = self.entity_description.convert( - self.statistics_src.statistics, self.entity_description.key - ) + self._set_statistics(self.statistics_src.statistics) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 0ff635578ea..fffcb2ca9dd 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -113,6 +113,16 @@ "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]", "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]" }, + "rf_region": { + "title": "Z-Wave region", + "description": "Select the RF region for your Z-Wave network.", + "data": { + "rf_region": "RF region" + }, + "data_description": { + "rf_region": "The radio frequency region for your Z-Wave network. This must match the region of your Z-Wave devices." + } + }, "start_addon": { "title": "Configuring add-on" }, diff --git a/homeassistant/const.py b/homeassistant/const.py index 9ddbac360af..5058f988958 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b38b7e6692..834e04abbf0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ audioop-lts==0.2.1 av==13.1.0 awesomeversion==25.5.0 bcrypt==4.3.0 -bleak-retry-connector==4.0.1 +bleak-retry-connector==4.0.2 bleak==1.0.1 bluetooth-adapters==2.0.0 bluetooth-auto-recovery==1.5.2 @@ -38,7 +38,7 @@ habluetooth==4.0.2 hass-nabucasa==0.111.2 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250811.0 +home-assistant-frontend==20250811.1 home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 @@ -221,3 +221,6 @@ num2words==0.5.14 # downgraded or upgraded by custom components # This ensures all use the same version pymodbus==3.9.2 + +# Some packages don't support gql 4.0.0 yet +gql<4.0.0 diff --git a/pyproject.toml b/pyproject.toml index ced768ae63e..c8199739592 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.8.2" +version = "2025.8.3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." diff --git a/requirements_all.txt b/requirements_all.txt index 8543aaedeaa..bef2cc417b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==4.0.0 +aioamazondevices==5.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -375,7 +375,7 @@ aioridwell==2024.01.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.8.0 +aiorussound==4.8.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 @@ -625,7 +625,7 @@ bizkaibus==0.1.1 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.0.1 +bleak-retry-connector==4.0.2 # homeassistant.components.bluetooth bleak==1.0.1 @@ -667,7 +667,7 @@ bond-async==0.2.1 bosch-alarm-mode2==0.4.6 # homeassistant.components.bosch_shc -boschshcpy==0.2.91 +boschshcpy==0.2.107 # homeassistant.components.amazon_polly # homeassistant.components.route53 @@ -1171,10 +1171,10 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.78 +holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250811.0 +home-assistant-frontend==20250811.1 # homeassistant.components.conversation home-assistant-intents==2025.7.30 @@ -1240,7 +1240,7 @@ ihcsdk==2.8.5 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.3 +imgw_pib==1.5.4 # homeassistant.components.incomfort incomfort-client==0.6.9 @@ -1594,7 +1594,7 @@ ondilo==0.5.0 onedrive-personal-sdk==0.0.14 # homeassistant.components.onvif -onvif-zeep-async==4.0.3 +onvif-zeep-async==4.0.4 # homeassistant.components.opengarage open-garage==0.2.0 @@ -1628,7 +1628,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.15.1 +opower==0.15.2 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1852,7 +1852,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==9.2.1 +pyatmo==9.2.3 # homeassistant.components.apple_tv pyatv==0.16.1 @@ -2352,7 +2352,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.1 # homeassistant.components.smartthings -pysmartthings==3.2.8 +pysmartthings==3.2.9 # homeassistant.components.smarty pysmarty2==0.10.2 @@ -2478,7 +2478,7 @@ python-miio==0.5.12 python-mpd2==3.1.1 # homeassistant.components.mystrom -python-mystrom==2.4.0 +python-mystrom==2.5.0 # homeassistant.components.open_router python-open-router==0.3.1 @@ -2660,7 +2660,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.1 +renault-api==0.4.0 # homeassistant.components.renson renson-endura-delta==1.7.2 @@ -3185,7 +3185,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.07.21 +yt-dlp[default]==2025.08.11 # homeassistant.components.zabbix zabbix-utils==2.0.2 @@ -3194,7 +3194,7 @@ zabbix-utils==2.0.2 zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5.2 +zcc-helper==3.6 # homeassistant.components.zeroconf zeroconf==0.147.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 718f7794e56..e005f5f7764 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==4.0.0 +aioamazondevices==5.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -357,7 +357,7 @@ aioridwell==2024.01.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.8.0 +aiorussound==4.8.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 @@ -559,7 +559,7 @@ bimmer-connected[china]==0.17.2 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.0.1 +bleak-retry-connector==4.0.2 # homeassistant.components.bluetooth bleak==1.0.1 @@ -598,7 +598,7 @@ bond-async==0.2.1 bosch-alarm-mode2==0.4.6 # homeassistant.components.bosch_shc -boschshcpy==0.2.91 +boschshcpy==0.2.107 # homeassistant.components.aws botocore==1.37.1 @@ -1020,10 +1020,10 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.78 +holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250811.0 +home-assistant-frontend==20250811.1 # homeassistant.components.conversation home-assistant-intents==2025.7.30 @@ -1074,7 +1074,7 @@ igloohome-api==0.1.1 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.3 +imgw_pib==1.5.4 # homeassistant.components.incomfort incomfort-client==0.6.9 @@ -1362,7 +1362,7 @@ ondilo==0.5.0 onedrive-personal-sdk==0.0.14 # homeassistant.components.onvif -onvif-zeep-async==4.0.3 +onvif-zeep-async==4.0.4 # homeassistant.components.opengarage open-garage==0.2.0 @@ -1384,7 +1384,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.15.1 +opower==0.15.2 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1557,7 +1557,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==9.2.1 +pyatmo==9.2.3 # homeassistant.components.apple_tv pyatv==0.16.1 @@ -1955,7 +1955,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.1 # homeassistant.components.smartthings -pysmartthings==3.2.8 +pysmartthings==3.2.9 # homeassistant.components.smarty pysmarty2==0.10.2 @@ -2051,7 +2051,7 @@ python-miio==0.5.12 python-mpd2==3.1.1 # homeassistant.components.mystrom -python-mystrom==2.4.0 +python-mystrom==2.5.0 # homeassistant.components.open_router python-open-router==0.3.1 @@ -2206,7 +2206,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.1 +renault-api==0.4.0 # homeassistant.components.renson renson-endura-delta==1.7.2 @@ -2632,13 +2632,13 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.07.21 +yt-dlp[default]==2025.08.11 # homeassistant.components.zamg zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5.2 +zcc-helper==3.6 # homeassistant.components.zeroconf zeroconf==0.147.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index eb986fd8bb0..2855d7998c1 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -247,6 +247,9 @@ num2words==0.5.14 # downgraded or upgraded by custom components # This ensures all use the same version pymodbus==3.9.2 + +# Some packages don't support gql 4.0.0 yet +gql<4.0.0 """ GENERATED_MESSAGE = ( diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 99a1c255e60..86dda1aab9a 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -83,7 +83,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # - reasonX should be the name of the invalid dependency "adax": {"adax": {"async-timeout"}, "adax-local": {"async-timeout"}}, "airthings": {"airthings-cloud": {"async-timeout"}}, - "alexa_devices": {"marisa-trie": {"setuptools"}}, "ampio": {"asmog": {"async-timeout"}}, "apache_kafka": {"aiokafka": {"async-timeout"}}, "apple_tv": {"pyatv": {"async-timeout"}}, diff --git a/script/licenses.py b/script/licenses.py index d7819cba536..ef62d4970dd 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -202,6 +202,7 @@ EXCEPTIONS = { "pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6 "sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14 "tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5 + "ujson", # https://github.com/ultrajson/ultrajson/blob/main/LICENSE.txt } # fmt: off diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index 22596706862..3c68b7b7626 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -8,9 +8,9 @@ from aioamazondevices.const import DEVICE_TYPE_TO_MODEL import pytest from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN -from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME +from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME from tests.common import MockConfigEntry @@ -80,7 +80,6 @@ def mock_config_entry() -> MockConfigEntry: domain=DOMAIN, title="Amazon Test Account", data={ - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: {"session": "test-session"}, diff --git a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr index 95798fca817..0f3c3647e90 100644 --- a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr +++ b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr @@ -47,7 +47,6 @@ }), 'entry': dict({ 'data': dict({ - 'country': 'IT', 'login_data': dict({ 'session': 'test-session', }), diff --git a/tests/components/alexa_devices/test_config_flow.py b/tests/components/alexa_devices/test_config_flow.py index e1b2974184b..e4b0f8aa087 100644 --- a/tests/components/alexa_devices/test_config_flow.py +++ b/tests/components/alexa_devices/test_config_flow.py @@ -6,17 +6,16 @@ from aioamazondevices.exceptions import ( CannotAuthenticate, CannotConnect, CannotRetrieveData, - WrongCountry, ) import pytest from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import TEST_CODE, TEST_COUNTRY, TEST_PASSWORD, TEST_USERNAME +from .const import TEST_CODE, TEST_PASSWORD, TEST_USERNAME from tests.common import MockConfigEntry @@ -37,7 +36,6 @@ async def test_full_flow( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -46,7 +44,6 @@ async def test_full_flow( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: { @@ -63,7 +60,6 @@ async def test_full_flow( (CannotConnect, "cannot_connect"), (CannotAuthenticate, "invalid_auth"), (CannotRetrieveData, "cannot_retrieve_data"), - (WrongCountry, "wrong_country"), ], ) async def test_flow_errors( @@ -87,7 +83,6 @@ async def test_flow_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -102,7 +97,6 @@ async def test_flow_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -131,7 +125,6 @@ async def test_already_configured( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, diff --git a/tests/components/alexa_devices/test_init.py b/tests/components/alexa_devices/test_init.py index 3100cfe5fa9..c628a5e00e7 100644 --- a/tests/components/alexa_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -4,12 +4,14 @@ from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion -from homeassistant.components.alexa_devices.const import DOMAIN +from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from . import setup_integration -from .const import TEST_SERIAL_NUMBER +from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME from tests.common import MockConfigEntry @@ -28,3 +30,32 @@ async def test_device_info( ) assert device_entry is not None assert device_entry == snapshot + + +async def test_migrate_entry( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test successful migration of entry data.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Amazon Test Account", + data={ + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_LOGIN_DATA: {"session": "test-session"}, + }, + unique_id=TEST_USERNAME, + version=1, + minor_version=0, + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.minor_version == 1 + assert config_entry.data["site"] == f"https://www.amazon.{TEST_COUNTRY}" diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index 0e82d81f4e8..b4557fb2a4d 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -15,7 +15,11 @@ from amberelectric.models.spike_status import SpikeStatus from dateutil import parser import pytest -from homeassistant.components.amberelectric.const import CONF_SITE_ID, CONF_SITE_NAME +from homeassistant.components.amberelectric.const import ( + CONF_SITE_ID, + CONF_SITE_NAME, + REQUEST_TIMEOUT, +) from homeassistant.components.amberelectric.coordinator import AmberUpdateCoordinator from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant @@ -104,7 +108,9 @@ async def test_fetch_general_site(hass: HomeAssistant, current_price_api: Mock) result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_ONLY_SITE_ID, next=288 + GENERAL_ONLY_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance @@ -136,7 +142,9 @@ async def test_fetch_no_general_site( await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_ONLY_SITE_ID, next=288 + GENERAL_ONLY_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) @@ -150,7 +158,9 @@ async def test_fetch_api_error(hass: HomeAssistant, current_price_api: Mock) -> result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_ONLY_SITE_ID, next=288 + GENERAL_ONLY_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance @@ -201,7 +211,9 @@ async def test_fetch_general_and_controlled_load_site( result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_AND_CONTROLLED_SITE_ID, next=288 + GENERAL_AND_CONTROLLED_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance @@ -241,7 +253,9 @@ async def test_fetch_general_and_feed_in_site( result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_AND_FEED_IN_SITE_ID, next=288 + GENERAL_AND_FEED_IN_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance diff --git a/tests/components/imeon_inverter/snapshots/test_sensor.ambr b/tests/components/imeon_inverter/snapshots/test_sensor.ambr index fb59aa9dede..84e691bc8de 100644 --- a/tests/components/imeon_inverter/snapshots/test_sensor.ambr +++ b/tests/components/imeon_inverter/snapshots/test_sensor.ambr @@ -192,7 +192,7 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'Battery stored', 'platform': 'imeon_inverter', @@ -201,16 +201,16 @@ 'supported_features': 0, 'translation_key': 'battery_stored', 'unique_id': '111111111111111_battery_stored', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.imeon_inverter_battery_stored-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'energy_storage', + 'device_class': 'power', 'friendly_name': 'Imeon inverter Battery stored', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.imeon_inverter_battery_stored', @@ -1290,7 +1290,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1328,7 +1328,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Imeon inverter Monitoring self-consumption', - 'state_class': , + 'state_class': , 'unit_of_measurement': '%', }), 'context': , @@ -1345,7 +1345,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1383,7 +1383,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Imeon inverter Monitoring self-sufficiency', - 'state_class': , + 'state_class': , 'unit_of_measurement': '%', }), 'context': , @@ -2072,7 +2072,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -2094,7 +2094,7 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'PV consumed', 'platform': 'imeon_inverter', @@ -2103,16 +2103,16 @@ 'supported_features': 0, 'translation_key': 'pv_consumed', 'unique_id': '111111111111111_pv_consumed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.imeon_inverter_pv_consumed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'energy', + 'device_class': 'power', 'friendly_name': 'Imeon inverter PV consumed', - 'state_class': , - 'unit_of_measurement': , + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.imeon_inverter_pv_consumed', @@ -2128,7 +2128,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -2150,7 +2150,7 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'PV injected', 'platform': 'imeon_inverter', @@ -2159,16 +2159,16 @@ 'supported_features': 0, 'translation_key': 'pv_injected', 'unique_id': '111111111111111_pv_injected', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.imeon_inverter_pv_injected-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'energy', + 'device_class': 'power', 'friendly_name': 'Imeon inverter PV injected', - 'state_class': , - 'unit_of_measurement': , + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.imeon_inverter_pv_injected', diff --git a/tests/components/matter/test_valve.py b/tests/components/matter/test_valve.py index 36ab34cb64e..db64a5bacef 100644 --- a/tests/components/matter/test_valve.py +++ b/tests/components/matter/test_valve.py @@ -133,3 +133,22 @@ async def test_valve( command=clusters.ValveConfigurationAndControl.Commands.Open(targetLevel=100), ) matter_client.send_device_command.reset_mock() + + # test using set_position action to close valve + await hass.services.async_call( + "valve", + "set_valve_position", + { + "entity_id": entity_id, + "position": 0, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=matter_node.node_id, + endpoint_id=1, + command=clusters.ValveConfigurationAndControl.Commands.Close(), + ) + matter_client.send_device_command.reset_mock() diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index ba89405bc97..d9616572b2e 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -10,6 +10,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.nanoleaf.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -463,3 +464,59 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_abort_discovery_flow_with_user_flow(hass: HomeAssistant) -> None: + """Test abort discovery flow if user flow is already in progress.""" + with ( + patch( + "homeassistant.components.nanoleaf.config_flow.load_json_object", + return_value={}, + ), + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf", + return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), + ), + patch( + "homeassistant.components.nanoleaf.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={}, + ssdp_headers={ + "_host": TEST_HOST, + "nl-devicename": TEST_NAME, + "nl-deviceid": TEST_DEVICE_ID, + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] is None + assert result["step_id"] == "link" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "link" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # Verify the discovery flow was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/openai_conversation/test_entity.py b/tests/components/openai_conversation/test_entity.py index 58187bd63e9..c24cb5b3d79 100644 --- a/tests/components/openai_conversation/test_entity.py +++ b/tests/components/openai_conversation/test_entity.py @@ -63,6 +63,8 @@ async def test_format_structured_output() -> None: "item_value", ], "type": "object", + "additionalProperties": False, + "strict": True, }, "type": "array", }, diff --git a/tests/components/volvo/conftest.py b/tests/components/volvo/conftest.py index edd3f39998e..fedd3a6ec3f 100644 --- a/tests/components/volvo/conftest.py +++ b/tests/components/volvo/conftest.py @@ -9,7 +9,7 @@ from volvocarsapi.auth import TOKEN_URL from volvocarsapi.models import ( VolvoCarsAvailableCommand, VolvoCarsLocation, - VolvoCarsValueField, + VolvoCarsValueStatusField, VolvoCarsVehicle, ) @@ -98,7 +98,7 @@ async def mock_api(hass: HomeAssistant, full_model: str) -> AsyncGenerator[Async hass, "energy_state", full_model ) energy_state = { - key: VolvoCarsValueField.from_dict(value) + key: VolvoCarsValueStatusField.from_dict(value) for key, value in energy_state_data.items() } engine_status = await async_load_fixture_as_value_field( diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json b/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json index 968c759ab27..f3aff11585d 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { @@ -25,7 +25,7 @@ "isSupported": true }, "chargingCurrentLimit": { - "isSupported": true + "isSupported": false }, "chargingPower": { "isSupported": true diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_state.json b/tests/components/volvo/fixtures/ex30_2024/energy_state.json index 0170d1aa617..5973100d4ea 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_state.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_state.json @@ -50,7 +50,7 @@ }, "chargingPower": { "status": "ERROR", - "code": "NOT_SUPPORTED", - "message": "Resource is not supported for this vehicle" + "code": "PROPERTY_NOT_FOUND", + "message": "No valid value could be found for the requested property" } } diff --git a/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json b/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json index 968c759ab27..3523d51e071 100644 --- a/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json +++ b/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json index d8aa07ff0bb..331795f545b 100644 --- a/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json index e2f0cd13807..e198bfc8330 100644 --- a/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json @@ -40,9 +40,10 @@ "message": "Resource is not supported for this vehicle" }, "targetBatteryChargeLevel": { - "status": "ERROR", - "code": "NOT_SUPPORTED", - "message": "Resource is not supported for this vehicle" + "status": "OK", + "value": 80, + "unit": "percentage", + "updatedAt": "2024-09-22T09:40:12Z" }, "chargingPower": { "status": "ERROR", diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index 29e7e1e72a5..cdc6b44ff79 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -232,6 +232,62 @@ 'state': 'connected', }) # --- +# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_ex30_charging_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging power', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_power', + 'unique_id': 'yv1abcdefg1234567_charging_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Volvo EX30 Charging power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_ex30_charging_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2164,7 +2220,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '1386', }) # --- # name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_charging_power_status-entry] @@ -3601,6 +3657,58 @@ 'state': '30000', }) # --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_target_battery_charge_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Target battery charge level', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'target_battery_charge_level', + 'unique_id': 'yv1abcdefg1234567_target_battery_charge_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC60 Target battery charge level', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_target_battery_charge_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- # name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_engine_service-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/volvo/test_sensor.py b/tests/components/volvo/test_sensor.py index 2813c741286..a4b7a787117 100644 --- a/tests/components/volvo/test_sensor.py +++ b/tests/components/volvo/test_sensor.py @@ -68,4 +68,20 @@ async def test_skip_invalid_api_fields( with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): assert await setup_integration() - assert not hass.states.get(f"sensor.volvo_{short_model}_charging_power") + assert not hass.states.get(f"sensor.volvo_{short_model}_charging_current_limit") + + +@pytest.mark.parametrize( + "full_model", + ["ex30_2024"], +) +async def test_charging_power_value( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], +) -> None: + """Test if charging_power_value is zero if supported, but not charging.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): + assert await setup_integration() + + assert hass.states.get("sensor.volvo_ex30_charging_power").state == "0" diff --git a/tests/components/withings/snapshots/test_diagnostics.ambr b/tests/components/withings/snapshots/test_diagnostics.ambr index f7c704a2c49..bfd56fbc4d4 100644 --- a/tests/components/withings/snapshots/test_diagnostics.ambr +++ b/tests/components/withings/snapshots/test_diagnostics.ambr @@ -1,6 +1,18 @@ # serializer version: 1 # name: test_diagnostics_cloudhook_instance dict({ + 'devices': list([ + dict({ + 'battery': 'high', + 'device_id': '**REDACTED**', + 'device_type': 'Scale', + 'first_session_date': None, + 'hashed_device_id': '**REDACTED**', + 'last_session_date': '2023-09-04T22:39:39+00:00', + 'model': 5, + 'raw_model': 'Body+', + }), + ]), 'has_cloudhooks': True, 'has_valid_external_webhook_url': True, 'received_activity_data': False, @@ -64,6 +76,18 @@ # --- # name: test_diagnostics_polling_instance dict({ + 'devices': list([ + dict({ + 'battery': 'high', + 'device_id': '**REDACTED**', + 'device_type': 'Scale', + 'first_session_date': None, + 'hashed_device_id': '**REDACTED**', + 'last_session_date': '2023-09-04T22:39:39+00:00', + 'model': 5, + 'raw_model': 'Body+', + }), + ]), 'has_cloudhooks': False, 'has_valid_external_webhook_url': False, 'received_activity_data': False, @@ -127,6 +151,18 @@ # --- # name: test_diagnostics_webhook_instance dict({ + 'devices': list([ + dict({ + 'battery': 'high', + 'device_id': '**REDACTED**', + 'device_type': 'Scale', + 'first_session_date': None, + 'hashed_device_id': '**REDACTED**', + 'last_session_date': '2023-09-04T22:39:39+00:00', + 'model': 5, + 'raw_model': 'Body+', + }), + ]), 'has_cloudhooks': False, 'has_valid_external_webhook_url': True, 'received_activity_data': False, diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 52b840fb690..bab13666a29 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -198,6 +198,17 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]: client.driver.controller.data["sdkVersion"] = original_sdk_version +@pytest.fixture(name="set_country", autouse=True) +def set_country_fixture(hass: HomeAssistant) -> Generator[None]: + """Set the country for the test.""" + original_country = hass.config.country + # Set a default country to avoid asking the user to select it. + hass.config.country = "US" + yield + # Reset the country after the test. + hass.config.country = original_country + + async def test_manual(hass: HomeAssistant) -> None: """Test we create an entry with manual step.""" @@ -4601,3 +4612,324 @@ async def test_recommended_usb_discovery( } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info", "unload_entry") +async def test_addon_rf_region_new_network( + hass: HomeAssistant, + setup_entry: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test RF region selection for new network when country is None.""" + device = "/test" + hass.config.country = None + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_recommended"} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "usb_path": device, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "rf_region" + + # Check that all expected RF regions are available + + data_schema = result["data_schema"] + assert data_schema is not None + schema = data_schema.schema + rf_region_field = schema["rf_region"] + selector_options = rf_region_field.config["options"] + + expected_regions = [ + "Australia/New Zealand", + "China", + "Europe", + "Hong Kong", + "India", + "Israel", + "Japan", + "Korea", + "Russia", + "USA", + ] + + assert selector_options == expected_regions + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"rf_region": "Europe"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + # Verify RF region was set in addon config + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": device, + "s0_legacy_key": "", + "s2_access_control_key": "", + "s2_authenticated_key": "", + "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", + "rf_region": "Europe", + } + ), + ) + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert start_addon.call_count == 1 + assert start_addon.call_args == call("core_zwave_js") + assert setup_entry.call_count == 1 + + # avoid unload entry in teardown + entry = result["result"] + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +@pytest.mark.usefixtures("supervisor", "addon_running") +async def test_addon_rf_region_migrate_network( + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, + restart_addon: AsyncMock, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + get_server_version: AsyncMock, +) -> None: + """Test migration flow with add-on.""" + hass.config.country = None + version_info = get_server_version.return_value + entry = integration + assert client.connect.call_count == 1 + assert client.driver.controller.home_id == 3245146787 + assert entry.unique_id == "3245146787" + hass.config_entries.async_update_entry( + entry, + data={ + "url": "ws://localhost:3000", + "use_addon": True, + "usb_path": "/dev/ttyUSB0", + }, + ) + addon_options["device"] = "/dev/ttyUSB0" + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + + async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): + client.driver.controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, + ) + client.driver.controller.data["homeId"] = 3245146787 + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE + ) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_migrate"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + with patch("pathlib.Path.write_bytes") as mock_file: + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + assert len(events) == 1 + assert events[0].data["progress"] == 0.5 + events.clear() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "choose_serial_port" + data_schema = result["data_schema"] + assert data_schema is not None + assert data_schema.schema[CONF_USB_PATH] + # Ensure the old usb path is not in the list of options + with pytest.raises(InInvalid): + data_schema.schema[CONF_USB_PATH](addon_options["device"]) + + version_info.home_id = 5678 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USB_PATH: "/test", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "rf_region" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"rf_region": "Europe"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": "/test", + "rf_region": "Europe", + } + ), + ) + + await hass.async_block_till_done() + + assert restart_addon.call_args == call("core_zwave_js") + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert entry.unique_id == "5678" + version_info.home_id = 3245146787 + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 + + await hass.async_block_till_done() + assert client.connect.call_count == 4 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migration_successful" + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert entry.unique_id == "3245146787" + assert client.driver.controller.home_id == 3245146787 + + +@pytest.mark.usefixtures("supervisor", "addon_installed", "unload_entry") +@pytest.mark.parametrize(("country", "rf_region"), [("US", "Automatic"), (None, "USA")]) +async def test_addon_skip_rf_region( + hass: HomeAssistant, + setup_entry: AsyncMock, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + start_addon: AsyncMock, + country: str | None, + rf_region: str, +) -> None: + """Test RF region selection is skipped if not needed.""" + device = "/test" + addon_options["rf_region"] = rf_region + hass.config.country = country + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_recommended"} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "usb_path": device, + }, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + # Verify RF region was set in addon config + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": device, + "s0_legacy_key": "", + "s2_access_control_key": "", + "s2_authenticated_key": "", + "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", + "rf_region": rf_region, + } + ), + ) + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert start_addon.call_count == 1 + assert start_addon.call_args == call("core_zwave_js") + assert setup_entry.call_count == 1 + + # avoid unload entry in teardown + entry = result["result"] + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index c7b41449d43..e287c9e988f 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1045,6 +1045,183 @@ async def test_last_seen_statistics_sensors( assert state.state == "2024-01-01T12:00:00+00:00" +async def test_rssi_sensor_error( + hass: HomeAssistant, + zp3111: Node, + integration: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test rssi sensor error.""" + entity_id = "sensor.4_in_1_sensor_signal_strength" + + entity_registry.async_update_entity(entity_id, disabled_by=None) + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + + # Fire statistics updated event for node + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 7, # baseline + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "7" + + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 125, # no signal detected + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 127, # not available + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unavailable" + + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 126, # receiver saturated + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + + ENERGY_PRODUCTION_ENTITY_MAP = { "energy_production_power": { "state": 1.23,