diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 77c5d02bc56a..63cf8cfa3b67 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 8 + CACHE_VERSION: 1 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.10" diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 2128d874390f..4c328571d0e5 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -34,6 +34,9 @@ INPUT_FIELD_CODE = "code" DUMMY_SECRET = "FPPTH34D4E3MI2HG" +GOOGLE_AUTHENTICATOR_URL = "https://support.google.com/accounts/answer/1066447" +AUTHY_URL = "https://authy.com/" + def _generate_qr_code(data: str) -> str: """Generate a base64 PNG string represent QR Code image of data.""" @@ -229,6 +232,8 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]): "code": self._ota_secret, "url": self._url, "qr_code": self._image, + "google_authenticator_url": GOOGLE_AUTHENTICATOR_URL, + "authy_url": AUTHY_URL, }, errors=errors, ) diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py index 661e1b0a2987..331fdb729f57 100644 --- a/homeassistant/components/airnow/config_flow.py +++ b/homeassistant/components/airnow/config_flow.py @@ -26,6 +26,10 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +# Documentation URL for API key generation +_API_KEY_URL = "https://docs.airnowapi.org/account/request/" + + async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: """Validate the user input allows us to connect. @@ -114,6 +118,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN): ), } ), + description_placeholders={"api_key_url": _API_KEY_URL}, errors=errors, ) diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json index a69f67948cb8..f6ccf8583146 100644 --- a/homeassistant/components/airnow/strings.json +++ b/homeassistant/components/airnow/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "To generate API key go to https://docs.airnowapi.org/account/request/", + "description": "To generate API key go to {api_key_url}", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "latitude": "[%key:common::config_flow::data::latitude%]", diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 6e4b0b50c4c0..6134f6c1a5d8 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==1.0.1"] + "requirements": ["aioairzone==1.0.2"] } diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 43514727e971..a65d77de2bd2 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": "platinum", - "requirements": ["aioamazondevices==6.4.4"] + "requirements": ["aioamazondevices==6.4.6"] } diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index b1e80d716d89..cae69673cebd 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -5,7 +5,7 @@ "step": { "init": { "title": "Set up two-factor authentication using TOTP", - "description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**." + "description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator]({google_authenticator_url}) or [Authy]({authy_url}).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**." } }, "error": { diff --git a/homeassistant/components/bring/manifest.json b/homeassistant/components/bring/manifest.json index b2d42835cce7..b2177acb52f0 100644 --- a/homeassistant/components/bring/manifest.json +++ b/homeassistant/components/bring/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["bring_api"], "quality_scale": "platinum", - "requirements": ["bring-api==1.1.0"] + "requirements": ["bring-api==1.1.1"] } diff --git a/homeassistant/components/cync/manifest.json b/homeassistant/components/cync/manifest.json index b61a3165a1db..99088ad45768 100644 --- a/homeassistant/components/cync/manifest.json +++ b/homeassistant/components/cync/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "quality_scale": "bronze", - "requirements": ["pycync==0.4.1"] + "requirements": ["pycync==0.4.2"] } diff --git a/homeassistant/components/droplet/manifest.json b/homeassistant/components/droplet/manifest.json index f4a03ebfb21a..80ded2d23a76 100644 --- a/homeassistant/components/droplet/manifest.json +++ b/homeassistant/components/droplet/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/droplet", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["pydroplet==2.3.3"], + "requirements": ["pydroplet==2.3.4"], "zeroconf": ["_droplet._tcp.local."] } diff --git a/homeassistant/components/flume/config_flow.py b/homeassistant/components/flume/config_flow.py index cbe3f4983fa4..bdd4eb4cf51a 100644 --- a/homeassistant/components/flume/config_flow.py +++ b/homeassistant/components/flume/config_flow.py @@ -111,7 +111,12 @@ class FlumeConfigFlow(ConfigFlow, domain=DOMAIN): errors[CONF_PASSWORD] = "invalid_auth" return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + description_placeholders={ + "api_url": "https://portal.flumetech.com/settings#token" + }, ) async def async_step_reauth( diff --git a/homeassistant/components/flume/strings.json b/homeassistant/components/flume/strings.json index acdb8e35fe06..e138da2c4f0b 100644 --- a/homeassistant/components/flume/strings.json +++ b/homeassistant/components/flume/strings.json @@ -7,7 +7,7 @@ }, "step": { "user": { - "description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at https://portal.flumetech.com/settings#token", + "description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at {api_url}", "title": "Connect to your Flume account", "data": { "username": "[%key:common::config_flow::data::username%]", diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index e0aee9b5c2a0..6cd21c7f6b3a 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.82", "babel==2.15.0"] + "requirements": ["holidays==0.83", "babel==2.15.0"] } diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 3c50f78141b0..cb945df5ec8b 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -182,14 +182,6 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]): "Failed to listen to websocket. Trying to reconnect: %s", err, ) - if not hass.is_stopping: - await asyncio.sleep(self.reconnect_time) - self.reconnect_time = min(self.reconnect_time * 2, MAX_WS_RECONNECT_TIME) - entry.async_create_background_task( - hass, - self.client_listen(hass, entry, automower_client), - "reconnect_task", - ) def _should_poll(self) -> bool: """Return True if at least one mower is connected and at least one is not OFF.""" diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 03605cc738b0..b3881ed0f7c2 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_push", "loggers": ["aioautomower"], "quality_scale": "silver", - "requirements": ["aioautomower==2.2.1"] + "requirements": ["aioautomower==2.3.1"] } diff --git a/homeassistant/components/huum/climate.py b/homeassistant/components/huum/climate.py index af4e8cc3623f..c520880e6919 100644 --- a/homeassistant/components/huum/climate.py +++ b/homeassistant/components/huum/climate.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from .const import CONFIG_DEFAULT_MAX_TEMP, CONFIG_DEFAULT_MIN_TEMP from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator from .entity import HuumBaseEntity @@ -55,12 +56,12 @@ class HuumDevice(HuumBaseEntity, ClimateEntity): @property def min_temp(self) -> int: """Return configured minimal temperature.""" - return self.coordinator.data.sauna_config.min_temp + return self.coordinator.data.sauna_config.min_temp or CONFIG_DEFAULT_MIN_TEMP @property def max_temp(self) -> int: """Return configured maximum temperature.""" - return self.coordinator.data.sauna_config.max_temp + return self.coordinator.data.sauna_config.max_temp or CONFIG_DEFAULT_MAX_TEMP @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/huum/const.py b/homeassistant/components/huum/const.py index 177c035f0415..28609a7e9202 100644 --- a/homeassistant/components/huum/const.py +++ b/homeassistant/components/huum/const.py @@ -9,3 +9,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT, Platform. CONFIG_STEAMER = 1 CONFIG_LIGHT = 2 CONFIG_STEAMER_AND_LIGHT = 3 + +CONFIG_DEFAULT_MIN_TEMP = 40 +CONFIG_DEFAULT_MAX_TEMP = 110 diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 23b0062fc82e..8a7bd098d758 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -48,6 +48,8 @@ from homeassistant.util.network import is_link_local from .const import DOMAIN, LOGGER +DEVICES_URL = "https://developer.lametric.com/user/devices" + class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Handle a LaMetric config flow.""" @@ -164,6 +166,9 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return self.async_show_form( step_id="manual_entry", data_schema=vol.Schema(schema), + description_placeholders={ + "devices_url": DEVICES_URL, + }, errors=errors, ) diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index f3fa1e811121..acf692398301 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -24,7 +24,7 @@ }, "data_description": { "host": "The IP address or hostname of your LaMetric TIME on your network.", - "api_key": "You can find this API key in the [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices)." + "api_key": "You can find this API key in the [devices page in your LaMetric developer account]({devices_url})." } }, "cloud_select_device": { diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index da475e500050..3d3f946b3aac 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_SOURCE, CONF_UNIT_OF_MEASUREMENT, LIGHT_LUX, + PERCENTAGE, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfSpeed, @@ -50,6 +51,7 @@ DEVICE_CLASS_MAPPING = { pypck.lcn_defs.VarUnit.VOLT: SensorDeviceClass.VOLTAGE, pypck.lcn_defs.VarUnit.AMPERE: SensorDeviceClass.CURRENT, pypck.lcn_defs.VarUnit.PPM: SensorDeviceClass.CO2, + pypck.lcn_defs.VarUnit.PERCENT: SensorDeviceClass.HUMIDITY, } UNIT_OF_MEASUREMENT_MAPPING = { @@ -62,6 +64,7 @@ UNIT_OF_MEASUREMENT_MAPPING = { pypck.lcn_defs.VarUnit.VOLT: UnitOfElectricPotential.VOLT, pypck.lcn_defs.VarUnit.AMPERE: UnitOfElectricCurrent.AMPERE, pypck.lcn_defs.VarUnit.PPM: CONCENTRATION_PARTS_PER_MILLION, + pypck.lcn_defs.VarUnit.PERCENT: PERCENTAGE, } diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index 8323c0e19958..cd85de5c6278 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -202,5 +202,10 @@ class MotionBlindsFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="connect", data_schema=self._config_settings, errors=errors + step_id="connect", + data_schema=self._config_settings, + errors=errors, + description_placeholders={ + "documentation_url": "https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-api-key", + }, ) diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index 12060cd69f04..4d93dcef6c07 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -9,7 +9,7 @@ } }, "connect": { - "description": "You will need the 16 character API key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-api-key for instructions", + "description": "You will need the 16 character API key, see {documentation_url} for instructions", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py index 0c0e8b296cd0..c5797e3f8f18 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -63,7 +63,12 @@ class NightscoutConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + description_placeholders={ + "example_url": "https://myhomeassistant.duckdns.org:5423", + }, ) diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json index fd1d81e1273f..1f13ed17054a 100644 --- a/homeassistant/components/nightscout/strings.json +++ b/homeassistant/components/nightscout/strings.json @@ -3,10 +3,13 @@ "step": { "user": { "title": "Enter your Nightscout server information.", - "description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (optional): Only use if your instance is protected (auth_default_roles != readable).", "data": { "url": "[%key:common::config_flow::data::url%]", "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "url": "The address of your Nightscout instance. For example: {example_url}.", + "api_key": "Optional; only use it if your instance is protected (auth_default_roles != readable)." } } }, diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 5c02b6e972ee..6e89fd074b9c 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -110,6 +110,8 @@ async def _create_webhook( translation_placeholders={ "base_url": hass_url, "network_link": "https://my.home-assistant.io/redirect/network/", + "sample_ip": "192.168.1.10", + "sample_url": "http://192.168.1.10:8123", }, ) else: diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index ac6771bb1bd9..f170d56feda5 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -177,4 +177,5 @@ class NukiConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=self.add_suggested_values_to_schema(data_schema, user_input), errors=errors, + description_placeholders={"sample_ip": "192.168.1.25"}, ) diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 84e66c3db964..ecd5872aadd6 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -9,7 +9,7 @@ "encrypt_token": "Use an encrypted token for authentication." }, "data_description": { - "host": "The hostname or IP address of your Nuki bridge. For example: 192.168.1.25." + "host": "The hostname or IP address of your Nuki bridge. For example: {sample_ip}." } }, "reauth_confirm": { @@ -34,7 +34,7 @@ "issues": { "https_webhook": { "title": "Nuki webhook URL uses HTTPS (SSL)", - "description": "The Nuki bridge can not push events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device" + "description": "The Nuki bridge cannot push events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `{sample_url}` where `{sample_ip}` is the IP of the Home Assistant device" } }, "entity": { diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 7295a41da0f4..175fdfe63cd1 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["opower"], "quality_scale": "bronze", - "requirements": ["opower==0.15.7"] + "requirements": ["opower==0.15.8"] } diff --git a/homeassistant/components/osoenergy/config_flow.py b/homeassistant/components/osoenergy/config_flow.py index a47f90e3c043..3a6efc28ac11 100644 --- a/homeassistant/components/osoenergy/config_flow.py +++ b/homeassistant/components/osoenergy/config_flow.py @@ -16,6 +16,9 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) _SCHEMA_STEP_USER = vol.Schema({vol.Required(CONF_API_KEY): str}) +CONF_PORTAL_URL = "portal_url" +OSOENERGY_PORTAL_URL = "https://portal.osoenergy.no/" + class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a OSO Energy config flow.""" @@ -45,6 +48,7 @@ class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=_SCHEMA_STEP_USER, errors=errors, + description_placeholders={CONF_PORTAL_URL: OSOENERGY_PORTAL_URL}, ) async def get_user_email(self, subscription_key: str) -> str | None: @@ -66,4 +70,5 @@ class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): data_schema=self.add_suggested_values_to_schema( _SCHEMA_STEP_USER, self._get_reauth_entry().data ), + description_placeholders={CONF_PORTAL_URL: OSOENERGY_PORTAL_URL}, ) diff --git a/homeassistant/components/osoenergy/strings.json b/homeassistant/components/osoenergy/strings.json index 48b99749ca10..27129579e2ee 100644 --- a/homeassistant/components/osoenergy/strings.json +++ b/homeassistant/components/osoenergy/strings.json @@ -3,14 +3,14 @@ "step": { "user": { "title": "OSO Energy auth", - "description": "Enter the 'Subscription key' for your account generated at 'https://portal.osoenergy.no/'", + "description": "Enter the 'Subscription key' for your account generated at {portal_url}", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } }, "reauth": { "title": "OSO Energy auth", - "description": "Enter a new 'Subscription key' for your account generated at 'https://portal.osoenergy.no/'.", + "description": "Enter a new 'Subscription key' for your account generated at {portal_url}", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 7a0226837694..43c7b3f3044c 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -171,9 +171,27 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN): except TooManyAttemptsBannedException: errors["base"] = "too_many_attempts" except UnknownUserException: + # If the user has no supported CozyTouch devices on + # the Overkiz API server. Login will return unknown user. + if user_input[CONF_HUB] in { + Server.ATLANTIC_COZYTOUCH, + Server.SAUTER_COZYTOUCH, + Server.THERMOR_COZYTOUCH, + }: + description_placeholders["unsupported_device"] = "CozyTouch" # Somfy Protect accounts are not supported since they don't use # the Overkiz API server. Login will return unknown user. - description_placeholders["unsupported_device"] = "Somfy Protect" + elif user_input[CONF_HUB] in { + Server.SOMFY_AMERICA, + Server.SOMFY_DEVELOPER_MODE, + Server.SOMFY_EUROPE, + Server.SOMFY_OCEANIA, + }: + description_placeholders["unsupported_device"] = "Somfy Protect" + # Fallback for other unknown devices + else: + description_placeholders["unsupported_device"] = "Unknown" + errors["base"] = "unsupported_hardware" except Exception: # noqa: BLE001 errors["base"] = "unknown" diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 6f0001691a62..ce783c4eb079 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -55,7 +55,7 @@ "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", "unknown": "[%key:common::config_flow::error::unknown%]", - "unsupported_hardware": "Your {unsupported_device} hardware is not supported by this integration." + "unsupported_hardware": "Your {unsupported_device} hardware is not using the Overkiz platform and can't be supported by this integration." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/probe_plus/coordinator.py b/homeassistant/components/probe_plus/coordinator.py index b712e3fc84bf..1e37340726b1 100644 --- a/homeassistant/components/probe_plus/coordinator.py +++ b/homeassistant/components/probe_plus/coordinator.py @@ -8,11 +8,15 @@ import logging from pyprobeplus import ProbePlusDevice from pyprobeplus.exceptions import ProbePlusDeviceNotFound, ProbePlusError +from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from .const import DOMAIN + type ProbePlusConfigEntry = ConfigEntry[ProbePlusDataUpdateCoordinator] _LOGGER = logging.getLogger(__name__) @@ -39,8 +43,17 @@ class ProbePlusDataUpdateCoordinator(DataUpdateCoordinator[None]): config_entry=entry, ) + available_scanners = bluetooth.async_scanner_count(hass, connectable=True) + + if available_scanners == 0: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="no_bleak_scanner", + ) + self.device: ProbePlusDevice = ProbePlusDevice( address_or_ble_device=entry.data[CONF_ADDRESS], + scanner=bluetooth.async_get_scanner(hass), name=entry.title, notify_callback=self.async_update_listeners, ) diff --git a/homeassistant/components/probe_plus/manifest.json b/homeassistant/components/probe_plus/manifest.json index 1d3bd549761b..160c231897c2 100644 --- a/homeassistant/components/probe_plus/manifest.json +++ b/homeassistant/components/probe_plus/manifest.json @@ -15,5 +15,5 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["pyprobeplus==1.1.0"] + "requirements": ["pyprobeplus==1.1.2"] } diff --git a/homeassistant/components/probe_plus/strings.json b/homeassistant/components/probe_plus/strings.json index 45fd4be39ce0..bd2ccac85146 100644 --- a/homeassistant/components/probe_plus/strings.json +++ b/homeassistant/components/probe_plus/strings.json @@ -45,5 +45,10 @@ "name": "Relay voltage" } } + }, + "exceptions": { + "no_bleak_scanner": { + "message": "No compatible Bluetooth scanner found." + } } } diff --git a/homeassistant/components/pterodactyl/entity.py b/homeassistant/components/pterodactyl/entity.py index 49fd65af4765..28733e9f7e8d 100644 --- a/homeassistant/components/pterodactyl/entity.py +++ b/homeassistant/components/pterodactyl/entity.py @@ -1,5 +1,7 @@ """Base entity for the Pterodactyl integration.""" +from yarl import URL + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.helpers.device_registry import DeviceInfo @@ -33,7 +35,9 @@ class PterodactylEntity(CoordinatorEntity[PterodactylCoordinator]): name=self.game_server_data.name, model=self.game_server_data.name, model_id=self.game_server_data.uuid, - configuration_url=f"{config_entry.data[CONF_URL]}/server/{identifier}", + configuration_url=str( + URL(config_entry.data[CONF_URL]) / "server" / identifier + ), ) @property diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index cc32bd2e56f6..4956d204a98c 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -91,7 +91,12 @@ class RachioConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + description_placeholders={ + "api_key_url": "https://app.rach.io/", + }, ) async def async_step_homekit( diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json index ea3c8911463d..f63454cb185a 100644 --- a/homeassistant/components/rachio/strings.json +++ b/homeassistant/components/rachio/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Connect to your Rachio device", - "description": "You will need the API key from https://app.rach.io/. Go to Settings, then select 'GET API KEY'.", + "description": "You will need the API key from {api_key_url}. Go to Settings, then select 'GET API KEY'.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index d100999527fa..8692ff403669 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -48,7 +48,7 @@ from .const import ( DEFAULT_OFF_DELAY = 2.0 -CONNECT_TIMEOUT = 30.0 +CONNECT_TIMEOUT = 60.0 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensorpush_cloud/config_flow.py b/homeassistant/components/sensorpush_cloud/config_flow.py index d06fde2eba1e..8cf68e091349 100644 --- a/homeassistant/components/sensorpush_cloud/config_flow.py +++ b/homeassistant/components/sensorpush_cloud/config_flow.py @@ -61,4 +61,7 @@ class SensorPushCloudConfigFlow(ConfigFlow, domain=DOMAIN): } ), errors=errors, + description_placeholders={ + "dashboard_url": "https://dashboard.sensorpush.com/", + }, ) diff --git a/homeassistant/components/sensorpush_cloud/strings.json b/homeassistant/components/sensorpush_cloud/strings.json index 8467a123b6f8..15db60e0a10d 100644 --- a/homeassistant/components/sensorpush_cloud/strings.json +++ b/homeassistant/components/sensorpush_cloud/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "To activate API access, log in to the [Gateway Cloud Dashboard](https://dashboard.sensorpush.com/) and agree to the terms of service. Devices are not available until activated with the SensorPush app on iOS or Android.", + "description": "To activate API access, log in to the [Gateway Cloud Dashboard]({dashboard_url}) and agree to the terms of service. Devices are not available until activated with the SensorPush app on iOS or Android.", "data": { "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index ced5f46be3a8..6107301ab5a4 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1554,8 +1554,7 @@ RPC_SENSORS: Final = { "number_energy_charge": RpcSensorDescription( key="number", sub_key="value", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, @@ -1599,8 +1598,7 @@ RPC_SENSORS: Final = { key="object", sub_key="value", value=lambda status, _: float(status["counter"]["total"]), - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -1611,8 +1609,7 @@ RPC_SENSORS: Final = { sub_key="value", name="Total Active Energy", value=lambda status, _: float(status["total_act_energy"]), - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -1623,8 +1620,7 @@ RPC_SENSORS: Final = { sub_key="value", name="Total Power", value=lambda status, _: float(status["total_power"]), - native_unit_of_measurement=UnitOfPower.WATT, - suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=2, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 68974fe118fe..6494b84981bd 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -27,6 +27,10 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER CONF_AUTH_CODE = "auth_code" +CONF_DOCUMENTATION_URL = "documentation_url" +DOCUMENTATION_URL = ( + "https://home-assistant.io/integrations/simplisafe#getting-an-authorization-code" +) STEP_USER_SCHEMA = vol.Schema( { @@ -84,7 +88,10 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=STEP_USER_SCHEMA, - description_placeholders={CONF_URL: self._oauth_values.auth_url}, + description_placeholders={ + CONF_URL: self._oauth_values.auth_url, + CONF_DOCUMENTATION_URL: DOCUMENTATION_URL, + }, ) auth_code = user_input[CONF_AUTH_CODE] @@ -102,7 +109,10 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_SCHEMA, errors={CONF_AUTH_CODE: "invalid_auth_code_length"}, - description_placeholders={CONF_URL: self._oauth_values.auth_url}, + description_placeholders={ + CONF_URL: self._oauth_values.auth_url, + CONF_DOCUMENTATION_URL: DOCUMENTATION_URL, + }, ) errors = {} @@ -124,7 +134,10 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors, - description_placeholders={CONF_URL: self._oauth_values.auth_url}, + description_placeholders={ + CONF_URL: self._oauth_values.auth_url, + CONF_DOCUMENTATION_URL: DOCUMENTATION_URL, + }, ) simplisafe_user_id = str(simplisafe.user_id) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 992160350809..1f12b1ed65f7 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL.", + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({documentation_url}) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL.", "data": { "auth_code": "Authorization Code" } diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index a899b562f360..0f1983fc21d7 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -117,6 +117,9 @@ class StarlineFlowHandler(ConfigFlow, domain=DOMAIN): } ), errors=errors, + description_placeholders={ + "developer_account_url": "https://my.starline.ru/developer", + }, ) @callback diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json index b3ce755778ee..9452e0b47570 100644 --- a/homeassistant/components/starline/strings.json +++ b/homeassistant/components/starline/strings.json @@ -3,7 +3,7 @@ "step": { "auth_app": { "title": "Application credentials", - "description": "Application ID and secret code from [StarLine developer account](https://my.starline.ru/developer)", + "description": "Application ID and secret code from [StarLine developer account]({developer_account_url})", "data": { "app_id": "App ID", "app_secret": "Secret" diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 60b57b1e87fa..fae75f4087e6 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -24,7 +24,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from .const import DATA_WAIT_TIMEOUT, DOMAIN +from .const import DATA_WAIT_TIMEOUT, DOMAIN, SYNTAX_KEYS_DOCUMENTATION_URL _LOGGER = logging.getLogger(__name__) @@ -132,7 +132,11 @@ class SystemBridgeConfigFlow( """Handle the initial step.""" if user_input is None: return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + description_placeholders={ + "syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL + }, ) errors, info = await _async_get_info(self.hass, user_input) @@ -144,7 +148,12 @@ class SystemBridgeConfigFlow( return self.async_create_entry(title=info["hostname"], data=user_input) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + description_placeholders={ + "syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL + }, ) async def async_step_authenticate( @@ -174,7 +183,10 @@ class SystemBridgeConfigFlow( return self.async_show_form( step_id="authenticate", data_schema=STEP_AUTHENTICATE_DATA_SCHEMA, - description_placeholders={"name": self._name}, + description_placeholders={ + "name": self._name, + "syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL, + }, errors=errors, ) diff --git a/homeassistant/components/system_bridge/const.py b/homeassistant/components/system_bridge/const.py index 235d7e6b9869..72160b86daae 100644 --- a/homeassistant/components/system_bridge/const.py +++ b/homeassistant/components/system_bridge/const.py @@ -4,6 +4,8 @@ from typing import Final from systembridgemodels.modules import Module +SYNTAX_KEYS_DOCUMENTATION_URL = "http://robotjs.io/docs/syntax#keys" + DOMAIN = "system_bridge" MODULES: Final[list[Module]] = [ diff --git a/homeassistant/components/system_bridge/strings.json b/homeassistant/components/system_bridge/strings.json index 0cca826684a6..7dd46aa0ecbf 100644 --- a/homeassistant/components/system_bridge/strings.json +++ b/homeassistant/components/system_bridge/strings.json @@ -194,7 +194,7 @@ }, "key": { "name": "Key", - "description": "Key to press. List available here: http://robotjs.io/docs/syntax#keys." + "description": "Key to press. List available here: {syntax_keys_documentation_url}." } } }, diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 8cf5f8b2b584..db82251f5bbc 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -134,7 +134,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - api = tesla.vehicles.createSigned(vin) else: api = tesla.vehicles.createFleet(vin) - coordinator = TeslaFleetVehicleDataCoordinator(hass, entry, api, product) + coordinator = TeslaFleetVehicleDataCoordinator( + hass, entry, api, product, Scope.VEHICLE_LOCATION in scopes + ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/tesla_fleet/coordinator.py b/homeassistant/components/tesla_fleet/coordinator.py index e3a31a2c0dca..59d1c2507035 100644 --- a/homeassistant/components/tesla_fleet/coordinator.py +++ b/homeassistant/components/tesla_fleet/coordinator.py @@ -39,9 +39,9 @@ ENDPOINTS = [ VehicleDataEndpoint.CHARGE_STATE, VehicleDataEndpoint.CLIMATE_STATE, VehicleDataEndpoint.DRIVE_STATE, - VehicleDataEndpoint.LOCATION_DATA, VehicleDataEndpoint.VEHICLE_STATE, VehicleDataEndpoint.VEHICLE_CONFIG, + VehicleDataEndpoint.LOCATION_DATA, ] @@ -65,6 +65,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): updated_once: bool pre2021: bool last_active: datetime + endpoints: list[VehicleDataEndpoint] def __init__( self, @@ -72,6 +73,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): config_entry: TeslaFleetConfigEntry, api: VehicleFleet, product: dict, + location: bool, ) -> None: """Initialize TeslaFleet Vehicle Update Coordinator.""" super().__init__( @@ -85,6 +87,11 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.data = flatten(product) self.updated_once = False self.last_active = datetime.now() + self.endpoints = ( + ENDPOINTS + if location + else [ep for ep in ENDPOINTS if ep != VehicleDataEndpoint.LOCATION_DATA] + ) async def _async_update_data(self) -> dict[str, Any]: """Update vehicle data using TeslaFleet API.""" @@ -97,7 +104,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): if self.data["state"] != TeslaFleetState.ONLINE: return self.data - response = await self.api.vehicle_data(endpoints=ENDPOINTS) + response = await self.api.vehicle_data(endpoints=self.endpoints) data = response["response"] except VehicleOffline: @@ -251,11 +258,14 @@ class TeslaFleetEnergySiteHistoryCoordinator(DataUpdateCoordinator[dict[str, Any raise UpdateFailed("Received invalid data") # Add all time periods together - output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0) + output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None) for period in data.get("time_series", []): for key in ENERGY_HISTORY_FIELDS: if key in period: - output[key] += period[key] + if output[key] is None: + output[key] = period[key] + else: + output[key] += period[key] return output diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index eed00ebc64f8..a73424bdd288 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -199,10 +199,13 @@ class TeslemetryEnergyHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise UpdateFailed("Received invalid data") # Add all time periods together - output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0) - for period in data["time_series"]: + output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None) + for period in data.get("time_series", []): for key in ENERGY_HISTORY_FIELDS: if key in period: - output[key] += period[key] + if output[key] is None: + output[key] = period[key] + else: + output[key] += period[key] return output diff --git a/homeassistant/components/thethingsnetwork/config_flow.py b/homeassistant/components/thethingsnetwork/config_flow.py index 412c5da4ef9f..3d1952068eab 100644 --- a/homeassistant/components/thethingsnetwork/config_flow.py +++ b/homeassistant/components/thethingsnetwork/config_flow.py @@ -82,7 +82,14 @@ class TTNFlowHandler(ConfigFlow, domain=DOMAIN): ), user_input, ) - return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + return self.async_show_form( + step_id="user", + data_schema=schema, + errors=errors, + description_placeholders={ + "instructions_url": "https://www.thethingsindustries.com/docs/integrations/adding-applications/" + }, + ) async def async_step_reauth( self, entry_data: Mapping[str, Any] diff --git a/homeassistant/components/thethingsnetwork/strings.json b/homeassistant/components/thethingsnetwork/strings.json index 8b3eb7b53c4c..4caf41881659 100644 --- a/homeassistant/components/thethingsnetwork/strings.json +++ b/homeassistant/components/thethingsnetwork/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Connect to The Things Network v3", - "description": "Enter the API hostname, application ID and API key to use with Home Assistant.\n\n[Read the instructions](https://www.thethingsindustries.com/docs/integrations/adding-applications/) on how to register your application and create an API key.", + "description": "Enter the API hostname, application ID and API key to use with Home Assistant.\n\n[Read the instructions]({instructions_url}) on how to register your application and create an API key.", "data": { "host": "[%key:common::config_flow::data::host%]", "app_id": "Application ID", diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py index cce41b174988..71674a646cb5 100644 --- a/homeassistant/components/tomorrowio/config_flow.py +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -172,4 +172,7 @@ class TomorrowioConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=_get_config_schema(self.hass, self.source, user_input), errors=errors, + description_placeholders={ + "signup_link": "[Tomorrow.io](https://app.tomorrow.io/signup)" + }, ) diff --git a/homeassistant/components/tomorrowio/strings.json b/homeassistant/components/tomorrowio/strings.json index 033b338f1a4c..457123a9febe 100644 --- a/homeassistant/components/tomorrowio/strings.json +++ b/homeassistant/components/tomorrowio/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "To get an API key, sign up at [Tomorrow.io](https://app.tomorrow.io/signup).", + "description": "To get an API key, sign up at {signup_link}.", "data": { "name": "[%key:common::config_flow::data::name%]", "api_key": "[%key:common::config_flow::data::api_key%]", diff --git a/homeassistant/components/uptime_kuma/coordinator.py b/homeassistant/components/uptime_kuma/coordinator.py index df64b12f8e9b..98f452bf7a8c 100644 --- a/homeassistant/components/uptime_kuma/coordinator.py +++ b/homeassistant/components/uptime_kuma/coordinator.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -89,7 +89,8 @@ def async_migrate_entities_unique_ids( """Migrate unique_ids in the entity registry after updating Uptime Kuma.""" if ( - coordinator.version is coordinator.api.version + coordinator.version is None + or coordinator.version.version == coordinator.api.version.version or int(coordinator.api.version.major) < 2 ): return @@ -116,6 +117,32 @@ def async_migrate_entities_unique_ids( new_unique_id=f"{registry_entry.config_entry_id}_{monitor.monitor_id!s}_{registry_entry.translation_key}", ) + # migrate device identifiers and update version + device_reg = dr.async_get(hass) + for monitor in metrics.values(): + if device := device_reg.async_get_device( + {(DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor.monitor_name!s}")} + ): + new_identifier = { + (DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor.monitor_id!s}") + } + device_reg.async_update_device( + device.id, + new_identifiers=new_identifier, + sw_version=coordinator.api.version.version, + ) + if device := device_reg.async_get_device( + {(DOMAIN, f"{coordinator.config_entry.entry_id}_update")} + ): + device_reg.async_update_device( + device.id, + sw_version=coordinator.api.version.version, + ) + + hass.async_create_task( + hass.config_entries.async_reload(coordinator.config_entry.entry_id) + ) + class UptimeKumaSoftwareUpdateCoordinator(DataUpdateCoordinator[LatestRelease]): """Uptime Kuma coordinator for retrieving update information.""" diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 65a3a5d20e6a..baba9d60290b 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -100,7 +100,10 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): """Return the currently set speed.""" current_level = self.device.state.fan_level - if self.device.state.mode == VS_FAN_MODE_MANUAL and current_level is not None: + if ( + self.device.state.mode in (VS_FAN_MODE_MANUAL, VS_FAN_MODE_NORMAL) + and current_level is not None + ): if current_level == 0: return 0 return ordered_list_item_to_percentage( @@ -182,7 +185,7 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): ) # Switch to manual mode if not already set - if self.device.state.mode != VS_FAN_MODE_MANUAL: + if self.device.state.mode not in (VS_FAN_MODE_MANUAL, VS_FAN_MODE_NORMAL): if not await self.device.set_manual_mode(): raise HomeAssistantError( "An error occurred while setting manual mode." diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index d0b420fe5c63..85fd43362e28 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.82"] + "requirements": ["holidays==0.83"] } diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 7bf139e9c3be..af7bbc1c6698 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -11,6 +11,7 @@ from aiomusiccast.features import ZoneFeature from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseError, BrowseMedia, MediaClass, MediaPlayerEntity, @@ -372,7 +373,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): ] if add_media_source: - with contextlib.suppress(media_source.BrowseError): + with contextlib.suppress(BrowseError): item = await media_source.async_browse_media( self.hass, None, diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index f33da34c1fc9..65ca84863f11 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.typing import ConfigType from . import api -from .const import ATTR_LORA_INFO, DOMAIN, YOLINK_EVENT +from .const import ATTR_LORA_INFO, DOMAIN, SUPPORTED_REMOTERS, YOLINK_EVENT from .coordinator import YoLinkCoordinator from .device_trigger import CONF_LONG_PRESS, CONF_SHORT_PRESS from .services import async_setup_services @@ -151,6 +151,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_pairing_mapping[parent_id] = device.device_id for device in yolink_home.get_devices(): + if ( + device.device_type == ATTR_DEVICE_SMART_REMOTER + and device.device_model_name not in SUPPORTED_REMOTERS + ): + continue paried_device: YoLinkDevice | None = None if ( paried_device_id := device_pairing_mapping.get(device.device_id) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 851b65e1a154..09bc2e1ff01d 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -44,3 +44,10 @@ DEV_MODEL_LEAK_STOP_YS5009 = "YS5009" DEV_MODEL_LEAK_STOP_YS5029 = "YS5029" DEV_MODEL_WATER_METER_YS5018_EC = "YS5018-EC" DEV_MODEL_WATER_METER_YS5018_UC = "YS5018-UC" + +SUPPORTED_REMOTERS = [ + DEV_MODEL_FLEX_FOB_YS3604_EC, + DEV_MODEL_FLEX_FOB_YS3604_UC, + DEV_MODEL_FLEX_FOB_YS3614_EC, + DEV_MODEL_FLEX_FOB_YS3614_UC, +] diff --git a/homeassistant/const.py b/homeassistant/const.py index 9e8b02b725c0..cbc33d17ebad 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -26,7 +26,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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/pyproject.toml b/pyproject.toml index a59e3c43942d..00812c2f0ea6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.10.3" +version = "2025.10.4" 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 aaa827afcc44..7fa8d6bd78b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,10 +182,10 @@ aioairq==0.4.7 aioairzone-cloud==0.7.2 # homeassistant.components.airzone -aioairzone==1.0.1 +aioairzone==1.0.2 # homeassistant.components.alexa_devices -aioamazondevices==6.4.4 +aioamazondevices==6.4.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -204,7 +204,7 @@ aioaseko==1.0.0 aioasuswrt==1.5.1 # homeassistant.components.husqvarna_automower -aioautomower==2.2.1 +aioautomower==2.3.1 # homeassistant.components.azure_devops aioazuredevops==2.2.2 @@ -682,7 +682,7 @@ boto3==1.37.1 botocore==1.37.1 # homeassistant.components.bring -bring-api==1.1.0 +bring-api==1.1.1 # homeassistant.components.broadlink broadlink==0.19.0 @@ -1183,7 +1183,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.82 +holidays==0.83 # homeassistant.components.frontend home-assistant-frontend==20251001.4 @@ -1649,7 +1649,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.15.7 +opower==0.15.8 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1933,7 +1933,7 @@ pycsspeechtts==1.0.8 # pycups==2.0.4 # homeassistant.components.cync -pycync==0.4.1 +pycync==0.4.2 # homeassistant.components.daikin pydaikin==2.17.1 @@ -1966,7 +1966,7 @@ pydrawise==2025.9.0 pydroid-ipcam==3.0.0 # homeassistant.components.droplet -pydroplet==2.3.3 +pydroplet==2.3.4 # homeassistant.components.ebox pyebox==1.1.4 @@ -2290,7 +2290,7 @@ pypoint==3.0.0 pyportainer==1.0.3 # homeassistant.components.probe_plus -pyprobeplus==1.1.0 +pyprobeplus==1.1.2 # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 691f765e6558..7d48c5ac2da6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,10 +170,10 @@ aioairq==0.4.7 aioairzone-cloud==0.7.2 # homeassistant.components.airzone -aioairzone==1.0.1 +aioairzone==1.0.2 # homeassistant.components.alexa_devices -aioamazondevices==6.4.4 +aioamazondevices==6.4.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -192,7 +192,7 @@ aioaseko==1.0.0 aioasuswrt==1.5.1 # homeassistant.components.husqvarna_automower -aioautomower==2.2.1 +aioautomower==2.3.1 # homeassistant.components.azure_devops aioazuredevops==2.2.2 @@ -609,7 +609,7 @@ boschshcpy==0.2.107 botocore==1.37.1 # homeassistant.components.bring -bring-api==1.1.0 +bring-api==1.1.1 # homeassistant.components.broadlink broadlink==0.19.0 @@ -1032,7 +1032,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.82 +holidays==0.83 # homeassistant.components.frontend home-assistant-frontend==20251001.4 @@ -1405,7 +1405,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.15.7 +opower==0.15.8 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1623,7 +1623,7 @@ pycsspeechtts==1.0.8 # pycups==2.0.4 # homeassistant.components.cync -pycync==0.4.1 +pycync==0.4.2 # homeassistant.components.daikin pydaikin==2.17.1 @@ -1647,7 +1647,7 @@ pydrawise==2025.9.0 pydroid-ipcam==3.0.0 # homeassistant.components.droplet -pydroplet==2.3.3 +pydroplet==2.3.4 # homeassistant.components.ecoforest pyecoforest==0.4.0 @@ -1914,7 +1914,7 @@ pypoint==3.0.0 pyportainer==1.0.3 # homeassistant.components.probe_plus -pyprobeplus==1.1.0 +pyprobeplus==1.1.2 # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 793a5f568d23..f814503b8726 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -237,11 +237,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # pyopnsense > pbr > setuptools "pbr": {"setuptools"} }, - "opower": { - # https://github.com/arrow-py/arrow/issues/1169 (fixed not yet released) - # opower > arrow > types-python-dateutil - "arrow": {"types-python-dateutil"} - }, "pvpc_hourly_pricing": {"aiopvpc": {"async-timeout"}}, "remote_rpi_gpio": { # https://github.com/waveform80/colorzero/issues/9 @@ -322,8 +317,6 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = { "lyric": {"homeassistant": {"aiolyric"}}, # https://github.com/microBeesTech/pythonSDK/ "microbees": {"homeassistant": {"microbeespy"}}, - # https://github.com/tiagocoutinho/async_modbus - "nibe_heatpump": {"nibe": {"async-modbus"}}, # https://github.com/ejpenney/pyobihai "obihai": {"homeassistant": {"pyobihai"}}, # https://github.com/iamkubi/pydactyl diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index 6507eea1fcb1..759a21bc559d 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -25,6 +25,9 @@ async def test_form( ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} + assert result["description_placeholders"] == { + "api_key_url": "https://docs.airnowapi.org/account/request/" + } result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) assert result2["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr index 0895073e0aaf..19c05f52d8dc 100644 --- a/tests/components/airzone/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone/snapshots/test_diagnostics.ambr @@ -328,65 +328,64 @@ 'firmware': '3.31', 'full-name': 'Airzone [1] System', 'id': 1, - 'master-system-zone': '1:1', - 'master-zone': 1, - 'mode': 3, - 'model': 'C6', - 'modes': list([ + 'masters': list([ 1, - 4, - 2, - 3, - 5, ]), + 'masters-slaves': dict({ + '1': list([ + 2, + 3, + 4, + 5, + ]), + }), + 'model': 'C6', 'problems': False, 'q-adapt': 0, + 'slaves': list([ + 2, + 3, + 4, + 5, + ]), }), '2': dict({ 'available': True, 'full-name': 'Airzone [2] System', 'id': 2, - 'master-system-zone': '2:1', - 'master-zone': 1, - 'mode': 7, - 'modes': list([ - 7, + 'masters': list([ 1, ]), + 'masters-slaves': dict({ + '1': list([ + ]), + }), 'problems': False, }), '3': dict({ 'available': True, 'full-name': 'Airzone [3] System', 'id': 3, - 'master-system-zone': '3:1', - 'master-zone': 1, - 'mode': 7, - 'modes': list([ - 4, - 2, - 3, - 5, - 7, + 'masters': list([ 1, ]), + 'masters-slaves': dict({ + '1': list([ + ]), + }), 'problems': False, }), '4': dict({ 'available': True, 'full-name': 'Airzone [4] System', 'id': 4, - 'master-system-zone': '4:1', - 'master-zone': 1, - 'mode': 6, - 'modes': list([ + 'masters': list([ 1, - 2, - 3, - 4, - 5, - 6, ]), + 'masters-slaves': dict({ + '1': list([ + ]), + }), 'problems': False, }), }), diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index 271b381d32f1..031c7261981a 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -192,17 +192,11 @@ async def test_websocket_not_available( await hass.async_block_till_done() assert f"{error_msg} Trying to reconnect: Boom" in caplog.text - # Simulate a successful connection caplog.clear() - await mock_called.wait() - mock_called.clear() - await hass.async_block_till_done() - assert mock.call_count == 2 - assert "Trying to reconnect: Boom" not in caplog.text # Simulate hass shutting down await hass.async_stop() - assert mock.call_count == 2 + assert mock.call_count == 1 async def test_device_info( diff --git a/tests/components/huum/test_climate.py b/tests/components/huum/test_climate.py index ca7fcf811851..3e0a07f92325 100644 --- a/tests/components/huum/test_climate.py +++ b/tests/components/huum/test_climate.py @@ -11,6 +11,10 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) +from homeassistant.components.huum.const import ( + CONFIG_DEFAULT_MAX_TEMP, + CONFIG_DEFAULT_MIN_TEMP, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -76,3 +80,39 @@ async def test_set_temperature( ) mock_huum.turn_on.assert_called_once_with(60) + + +async def test_temperature_range( + hass: HomeAssistant, + mock_huum: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the temperature range.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + # API response. + state = hass.states.get(ENTITY_ID) + assert state.attributes["min_temp"] == 40 + assert state.attributes["max_temp"] == 110 + + # Empty/unconfigured API response should return default values. + mock_huum.sauna_config.min_temp = 0 + mock_huum.sauna_config.max_temp = 0 + + await mock_config_entry.runtime_data.async_refresh() + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes["min_temp"] == CONFIG_DEFAULT_MIN_TEMP + assert state.attributes["max_temp"] == CONFIG_DEFAULT_MAX_TEMP + + # Custom configured API response. + mock_huum.sauna_config.min_temp = 50 + mock_huum.sauna_config.max_temp = 80 + + await mock_config_entry.runtime_data.async_refresh() + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes["min_temp"] == 50 + assert state.attributes["max_temp"] == 80 diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 410c2ebb5f1d..f2d9b6c76391 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -255,6 +255,96 @@ async def test_form_invalid_auth_cloud( assert result["errors"] == {"base": error} +@pytest.mark.parametrize( + ("side_effect", "description_placeholder", "server"), + [ + (UnknownUserException, "CozyTouch", TEST_SERVER_COZYTOUCH), + (UnknownUserException, "Unknown", TEST_SERVER2), + ], +) +async def test_form_invalid_hardware_cloud( + hass: HomeAssistant, + side_effect: Exception, + description_placeholder: str, + server: str, +) -> None: + """Test we handle unsupported hardware (cloud).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"hub": server}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" + + with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": TEST_EMAIL, "password": TEST_PASSWORD}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "unsupported_hardware"} + assert result["description_placeholders"] == { + "unsupported_device": description_placeholder + } + + +@pytest.mark.parametrize( + ("side_effect", "description_placeholder", "server"), + [ + (UnknownUserException, "Somfy Protect", TEST_SERVER), + ], +) +async def test_form_invalid_hardware_cloud_local( + hass: HomeAssistant, + side_effect: Exception, + description_placeholder: str, + server: str, +) -> None: + """Test we handle unsupported hardware (cloud and local).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"hub": server}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_type": "cloud"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" + + with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": TEST_EMAIL, "password": TEST_PASSWORD}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "unsupported_hardware"} + assert result["description_placeholders"] == { + "unsupported_device": description_placeholder + } + + @pytest.mark.parametrize( ("side_effect", "error"), [ diff --git a/tests/components/shelly/snapshots/test_sensor.ambr b/tests/components/shelly/snapshots/test_sensor.ambr index 3e849287bd73..c77d6fa6091c 100644 --- a/tests/components/shelly/snapshots/test_sensor.ambr +++ b/tests/components/shelly/snapshots/test_sensor.ambr @@ -307,9 +307,6 @@ 'sensor': dict({ 'suggested_display_precision': 2, }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index a936626854d7..ab1a399fa20b 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -1714,7 +1714,7 @@ async def test_rpc_shelly_ev_sensors( } config["number:201"] = { "name": "Session energy", - "meta": {"ui": {"unit": "Wh", "view": "label"}}, + "meta": {"ui": {"unit": "kWh", "view": "label"}}, "role": "energy_charge", } config["number:202"] = { @@ -1726,7 +1726,7 @@ async def test_rpc_shelly_ev_sensors( status = deepcopy(mock_rpc_device.status) status["number:200"] = {"value": "charger_charging"} - status["number:201"] = {"value": 5000} + status["number:201"] = {"value": 5.0} status["number:202"] = {"value": 60} monkeypatch.setattr(mock_rpc_device, "status", status) diff --git a/tests/components/tesla_fleet/snapshots/test_sensor.ambr b/tests/components/tesla_fleet/snapshots/test_sensor.ambr index eab1441399fb..8db495adfab4 100644 --- a/tests/components/tesla_fleet/snapshots/test_sensor.ambr +++ b/tests/components/tesla_fleet/snapshots/test_sensor.ambr @@ -71,7 +71,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_battery_discharged-entry] @@ -146,7 +146,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_battery_exported-entry] @@ -1121,7 +1121,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_grid_exported_from_battery-entry] @@ -1881,7 +1881,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_load_power-entry] @@ -2178,7 +2178,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.energy_site_solar_power-entry] diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index 3645a0f434da..02f1ca940a67 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -9,6 +9,7 @@ from aiohttp.client_exceptions import ClientResponseError from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion +from tesla_fleet_api.const import Scope, VehicleDataEndpoint from tesla_fleet_api.exceptions import ( InvalidRegion, InvalidToken, @@ -36,6 +37,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr from . import setup_platform +from .conftest import create_config_entry from .const import VEHICLE_ASLEEP, VEHICLE_DATA_ALT from tests.common import MockConfigEntry, async_fire_time_changed @@ -497,3 +499,65 @@ async def test_bad_implementation( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert not result["errors"] + + +async def test_vehicle_without_location_scope( + hass: HomeAssistant, + expires_at: int, + mock_vehicle_data: AsyncMock, +) -> None: + """Test vehicle setup without VEHICLE_LOCATION scope excludes location endpoint.""" + + # Create config entry without VEHICLE_LOCATION scope + config_entry = create_config_entry( + expires_at, + [ + Scope.OPENID, + Scope.OFFLINE_ACCESS, + Scope.VEHICLE_DEVICE_DATA, + # Deliberately exclude Scope.VEHICLE_LOCATION + ], + ) + + await setup_platform(hass, config_entry) + assert config_entry.state is ConfigEntryState.LOADED + + # Verify that vehicle_data was called without LOCATION_DATA endpoint + mock_vehicle_data.assert_called() + call_args = mock_vehicle_data.call_args + endpoints = call_args.kwargs.get("endpoints", []) + + # Should not include LOCATION_DATA endpoint + assert VehicleDataEndpoint.LOCATION_DATA not in endpoints + + # Should include other endpoints + assert VehicleDataEndpoint.CHARGE_STATE in endpoints + assert VehicleDataEndpoint.CLIMATE_STATE in endpoints + assert VehicleDataEndpoint.DRIVE_STATE in endpoints + assert VehicleDataEndpoint.VEHICLE_STATE in endpoints + assert VehicleDataEndpoint.VEHICLE_CONFIG in endpoints + + +async def test_vehicle_with_location_scope( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + mock_vehicle_data: AsyncMock, +) -> None: + """Test vehicle setup with VEHICLE_LOCATION scope includes location endpoint.""" + await setup_platform(hass, normal_config_entry) + assert normal_config_entry.state is ConfigEntryState.LOADED + + # Verify that vehicle_data was called with LOCATION_DATA endpoint + mock_vehicle_data.assert_called() + call_args = mock_vehicle_data.call_args + endpoints = call_args.kwargs.get("endpoints", []) + + # Should include LOCATION_DATA endpoint when scope is present + assert VehicleDataEndpoint.LOCATION_DATA in endpoints + + # Should include all other endpoints + assert VehicleDataEndpoint.CHARGE_STATE in endpoints + assert VehicleDataEndpoint.CLIMATE_STATE in endpoints + assert VehicleDataEndpoint.DRIVE_STATE in endpoints + assert VehicleDataEndpoint.VEHICLE_STATE in endpoints + assert VehicleDataEndpoint.VEHICLE_CONFIG in endpoints diff --git a/tests/components/uptime_kuma/test_sensor.py b/tests/components/uptime_kuma/test_sensor.py index 25bd76505286..873c16c4174b 100644 --- a/tests/components/uptime_kuma/test_sensor.py +++ b/tests/components/uptime_kuma/test_sensor.py @@ -9,10 +9,11 @@ import pytest from pythonkuma import MonitorStatus, UptimeKumaMonitor, UptimeKumaVersion from syrupy.assertion import SnapshotAssertion +from homeassistant.components.uptime_kuma.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -53,6 +54,7 @@ async def test_migrate_unique_id( snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, + device_registry: dr.DeviceRegistry, ) -> None: """Snapshot test states of sensor platform.""" mock_pythonkuma.metrics.return_value = { @@ -87,7 +89,7 @@ async def test_migrate_unique_id( ) } mock_pythonkuma.version = UptimeKumaVersion( - version="2.0.0-beta.3", major="2", minor="0", patch="0-beta.3" + version="2.0.2", major="2", minor="0", patch="2" ) freezer.tick(timedelta(seconds=30)) async_fire_time_changed(hass) @@ -95,3 +97,10 @@ async def test_migrate_unique_id( assert (entity := entity_registry.async_get("sensor.monitor_status")) assert entity.unique_id == "123456789_1_status" + + assert ( + device := device_registry.async_get_device( + identifiers={(DOMAIN, f"{entity.config_entry_id}_1")} + ) + ) + assert device.sw_version == "2.0.2" diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 88b6bc64ebb3..a87f4963a763 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -670,7 +670,7 @@ 'display_status': 'off', 'friendly_name': 'SmartTowerFan', 'mode': 'normal', - 'percentage': None, + 'percentage': 0, 'percentage_step': 8.333333333333334, 'preset_mode': 'normal', 'preset_modes': list([