diff --git a/homeassistant/components/androidtv_remote/strings.json b/homeassistant/components/androidtv_remote/strings.json index 33970171d4018..e41cbcf9a762a 100644 --- a/homeassistant/components/androidtv_remote/strings.json +++ b/homeassistant/components/androidtv_remote/strings.json @@ -44,12 +44,12 @@ } }, "apps": { - "title": "Configure Android Apps", - "description": "Configure application id {app_id}", + "title": "Configure Android apps", + "description": "Configure application ID {app_id}", "data": { - "app_name": "Application Name", + "app_name": "Application name", "app_id": "Application ID", - "app_icon": "Application Icon", + "app_icon": "Application icon", "app_delete": "Check to delete this application" } } diff --git a/homeassistant/components/asuswrt/strings.json b/homeassistant/components/asuswrt/strings.json index bab40f281f59b..9d50f50c7e92b 100644 --- a/homeassistant/components/asuswrt/strings.json +++ b/homeassistant/components/asuswrt/strings.json @@ -31,8 +31,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "invalid_unique_id": "Impossible to determine a valid unique id for the device", - "no_unique_id": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible" + "invalid_unique_id": "Impossible to determine a valid unique ID for the device", + "no_unique_id": "A device without a valid unique ID is already configured. Configuration of multiple instances is not possible" } }, "options": { @@ -42,7 +42,7 @@ "consider_home": "Seconds to wait before considering a device away", "track_unknown": "Track unknown / unnamed devices", "interface": "The interface that you want statistics from (e.g. eth0, eth1 etc)", - "dnsmasq": "The location in the router of the dnsmasq.leases files", + "dnsmasq": "The location of the dnsmasq.leases file in the router", "require_ip": "Devices must have IP (for access point mode)" } } diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 1910f8a55fb2f..ba1c457561f4d 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -435,6 +435,7 @@ async def _async_upload_backup( # no point in continuing raise BackupManagerError(str(result)) from result if isinstance(result, BackupAgentError): + LOGGER.error("Error uploading to %s: %s", agent_ids[idx], result) agent_errors[agent_ids[idx]] = result continue if isinstance(result, Exception): diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index e25c077b57fa4..ef1ec6a8936fd 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.20.0", "dbus-fast==2.24.3", - "habluetooth==3.6.0" + "habluetooth==3.7.0" ] } diff --git a/homeassistant/components/bring/manifest.json b/homeassistant/components/bring/manifest.json index ff24a9913508a..71fe733ccf5d5 100644 --- a/homeassistant/components/bring/manifest.json +++ b/homeassistant/components/bring/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/bring", "integration_type": "service", "iot_class": "cloud_polling", + "loggers": ["bring_api"], "requirements": ["bring-api==0.9.1"] } diff --git a/homeassistant/components/cambridge_audio/strings.json b/homeassistant/components/cambridge_audio/strings.json index 9f5e031815b42..6041232fe65b2 100644 --- a/homeassistant/components/cambridge_audio/strings.json +++ b/homeassistant/components/cambridge_audio/strings.json @@ -12,7 +12,7 @@ } }, "discovery_confirm": { - "description": "Do you want to setup {name}?" + "description": "Do you want to set up {name}?" }, "reconfigure": { "description": "Reconfigure your Cambridge Audio Streamer.", @@ -28,7 +28,7 @@ "cannot_connect": "Failed to connect to Cambridge Audio device. Please make sure the device is powered up and connected to the network. Try power-cycling the device if it does not connect." }, "abort": { - "wrong_device": "This Cambridge Audio device does not match the existing device id. Please make sure you entered the correct IP address.", + "wrong_device": "This Cambridge Audio device does not match the existing device ID. Please make sure you entered the correct IP address.", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 725fc84adc341..4d718433fca2d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -516,6 +516,19 @@ def supported_features(self) -> CameraEntityFeature: """Flag supported features.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> CameraEntityFeature: + """Return the supported features as CameraEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = CameraEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + @cached_property def is_recording(self) -> bool: """Return true if the device is recording.""" @@ -569,7 +582,7 @@ def frontend_stream_type(self) -> StreamType | None: self._deprecate_attr_frontend_stream_type_logged = True return self._attr_frontend_stream_type - if CameraEntityFeature.STREAM not in self.supported_features: + if CameraEntityFeature.STREAM not in self.supported_features_compat: return None if ( self._webrtc_provider @@ -798,7 +811,9 @@ def async_update_token(self) -> None: async def async_internal_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_internal_added_to_hass() - self.__supports_stream = self.supported_features & CameraEntityFeature.STREAM + self.__supports_stream = ( + self.supported_features_compat & CameraEntityFeature.STREAM + ) await self.async_refresh_providers(write_state=False) async def async_refresh_providers(self, *, write_state: bool = True) -> None: @@ -838,7 +853,7 @@ async def _async_get_supported_webrtc_provider[_T]( self, fn: Callable[[HomeAssistant, Camera], Coroutine[None, None, _T | None]] ) -> _T | None: """Get first provider that supports this camera.""" - if CameraEntityFeature.STREAM not in self.supported_features: + if CameraEntityFeature.STREAM not in self.supported_features_compat: return None return await fn(self.hass, self) @@ -896,7 +911,7 @@ def _invalidate_camera_capabilities_cache(self) -> None: def camera_capabilities(self) -> CameraCapabilities: """Return the camera capabilities.""" frontend_stream_types = set() - if CameraEntityFeature.STREAM in self.supported_features: + if CameraEntityFeature.STREAM in self.supported_features_compat: if self._supports_native_sync_webrtc or self._supports_native_async_webrtc: # The camera has a native WebRTC implementation frontend_stream_types.add(StreamType.WEB_RTC) @@ -916,7 +931,8 @@ def async_write_ha_state(self) -> None: """ super().async_write_ha_state() if self.__supports_stream != ( - supports_stream := self.supported_features & CameraEntityFeature.STREAM + supports_stream := self.supported_features_compat + & CameraEntityFeature.STREAM ): self.__supports_stream = supports_stream self._invalidate_camera_capabilities_cache() diff --git a/homeassistant/components/cloud/backup.py b/homeassistant/components/cloud/backup.py index 57145e52c448c..b9da6dfb6a4c1 100644 --- a/homeassistant/components/cloud/backup.py +++ b/homeassistant/components/cloud/backup.py @@ -181,6 +181,11 @@ async def async_upload_backup( headers=details["headers"] | {"content-length": str(backup.size)}, timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h ) + _LOGGER.log( + logging.DEBUG if upload_status.status < 400 else logging.WARNING, + "Backup upload status: %s", + upload_status.status, + ) upload_status.raise_for_status() except (TimeoutError, ClientError) as err: raise BackupAgentError("Failed to upload backup") from err diff --git a/homeassistant/components/cookidoo/__init__.py b/homeassistant/components/cookidoo/__init__.py index bb78f2a569d6b..5d3c211e78d51 100644 --- a/homeassistant/components/cookidoo/__init__.py +++ b/homeassistant/components/cookidoo/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from cookidoo_api import Cookidoo, CookidooConfig, CookidooLocalizationConfig +from cookidoo_api import Cookidoo, CookidooConfig, get_localization_options from homeassistant.const import ( CONF_COUNTRY, @@ -22,15 +22,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: CookidooConfigEntry) -> bool: """Set up Cookidoo from a config entry.""" + localizations = await get_localization_options( + country=entry.data[CONF_COUNTRY].lower(), + language=entry.data[CONF_LANGUAGE], + ) + cookidoo = Cookidoo( async_get_clientsession(hass), CookidooConfig( email=entry.data[CONF_EMAIL], password=entry.data[CONF_PASSWORD], - localization=CookidooLocalizationConfig( - country_code=entry.data[CONF_COUNTRY].lower(), - language=entry.data[CONF_LANGUAGE], - ), + localization=localizations[0], ), ) diff --git a/homeassistant/components/cookidoo/config_flow.py b/homeassistant/components/cookidoo/config_flow.py index 120ab162a6cce..80487ed757f8c 100644 --- a/homeassistant/components/cookidoo/config_flow.py +++ b/homeassistant/components/cookidoo/config_flow.py @@ -10,7 +10,6 @@ Cookidoo, CookidooAuthException, CookidooConfig, - CookidooLocalizationConfig, CookidooRequestException, get_country_options, get_localization_options, @@ -219,18 +218,19 @@ async def validate_input( else: data_input[CONF_LANGUAGE] = ( await get_localization_options(country=data_input[CONF_COUNTRY].lower()) - )[0] # Pick any language to test login + )[0].language # Pick any language to test login + + localizations = await get_localization_options( + country=data_input[CONF_COUNTRY].lower(), + language=data_input[CONF_LANGUAGE], + ) - session = async_get_clientsession(self.hass) cookidoo = Cookidoo( - session, + async_get_clientsession(self.hass), CookidooConfig( email=data_input[CONF_EMAIL], password=data_input[CONF_PASSWORD], - localization=CookidooLocalizationConfig( - country_code=data_input[CONF_COUNTRY].lower(), - language=data_input[CONF_LANGUAGE], - ), + localization=localizations[0], ), ) try: diff --git a/homeassistant/components/cookidoo/manifest.json b/homeassistant/components/cookidoo/manifest.json index 59d58200fdfdf..b1a3e9c0267a2 100644 --- a/homeassistant/components/cookidoo/manifest.json +++ b/homeassistant/components/cookidoo/manifest.json @@ -6,6 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/cookidoo", "integration_type": "service", "iot_class": "cloud_polling", + "loggers": ["cookidoo_api"], "quality_scale": "silver", - "requirements": ["cookidoo-api==0.10.0"] + "requirements": ["cookidoo-api==0.11.2"] } diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 9ce526712f036..001bff51991be 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -300,6 +300,10 @@ def state_attributes(self) -> dict[str, Any]: def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" if (features := self._attr_supported_features) is not None: + if type(features) is int: # noqa: E721 + new_features = CoverEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features return features supported_features = ( diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 7d6887ad14ce9..2bb299722b7c5 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["openwebif"], - "requirements": ["openwebifpy==4.3.0"] + "requirements": ["openwebifpy==4.3.1"] } diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index ed80ad9aabfe6..43f18d4fffc03 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["eq3btsmart"], - "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==1.1.0"] + "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.0.0"] } diff --git a/homeassistant/components/esphome/bluetooth.py b/homeassistant/components/esphome/bluetooth.py index 37ae28df0ca3a..004bea1835ded 100644 --- a/homeassistant/components/esphome/bluetooth.py +++ b/homeassistant/components/esphome/bluetooth.py @@ -7,7 +7,6 @@ from aioesphomeapi import APIClient, DeviceInfo from bleak_esphome import connect_scanner -from bleak_esphome.backend.cache import ESPHomeBluetoothCache from homeassistant.components.bluetooth import async_register_scanner from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback @@ -28,10 +27,9 @@ def async_connect_scanner( entry_data: RuntimeEntryData, cli: APIClient, device_info: DeviceInfo, - cache: ESPHomeBluetoothCache, ) -> CALLBACK_TYPE: """Connect scanner.""" - client_data = connect_scanner(cli, device_info, cache, entry_data.available) + client_data = connect_scanner(cli, device_info, entry_data.available) entry_data.bluetooth_device = client_data.bluetooth_device client_data.disconnect_callbacks = entry_data.disconnect_callbacks scanner = client_data.scanner diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index aa46469c40e07..ed307b46fd6e9 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -6,8 +6,6 @@ from functools import cache from typing import Self -from bleak_esphome.backend.cache import ESPHomeBluetoothCache - from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder @@ -22,9 +20,6 @@ class DomainData: """Define a class that stores global esphome data in hass.data[DOMAIN].""" _stores: dict[str, ESPHomeStorage] = field(default_factory=dict) - bluetooth_cache: ESPHomeBluetoothCache = field( - default_factory=ESPHomeBluetoothCache - ) def get_entry_data(self, entry: ESPHomeConfigEntry) -> RuntimeEntryData: """Return the runtime entry data associated with this config entry. diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 007b4e791e17e..dfd318c0c7419 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -423,9 +423,7 @@ async def _on_connnect(self) -> None: if device_info.bluetooth_proxy_feature_flags_compat(api_version): entry_data.disconnect_callbacks.add( - async_connect_scanner( - hass, entry_data, cli, device_info, self.domain_data.bluetooth_cache - ) + async_connect_scanner(hass, entry_data, cli, device_info) ) if device_info.voice_assistant_feature_flags_compat(api_version) and ( diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 775ffbff4c8ff..b04fa4db428c7 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -18,7 +18,7 @@ "requirements": [ "aioesphomeapi==28.0.0", "esphome-dashboard-api==1.2.3", - "bleak-esphome==1.1.0" + "bleak-esphome==2.0.0" ], "zeroconf": ["_esphomelib._tcp.local."] } diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 190947e4c6f64..3ffddee1c7dfd 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -2,10 +2,11 @@ from datetime import datetime as dt import logging +from typing import Any import jwt from pyflick import FlickAPI -from pyflick.authentication import AbstractFlickAuth +from pyflick.authentication import SimpleFlickAuth from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET from homeassistant.config_entries import ConfigEntry @@ -93,16 +94,22 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -class HassFlickAuth(AbstractFlickAuth): +class HassFlickAuth(SimpleFlickAuth): """Implementation of AbstractFlickAuth based on a Home Assistant entity config.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: FlickConfigEntry) -> None: """Flick authentication based on a Home Assistant entity config.""" - super().__init__(aiohttp_client.async_get_clientsession(hass)) + super().__init__( + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + client_id=entry.data.get(CONF_CLIENT_ID, DEFAULT_CLIENT_ID), + client_secret=entry.data.get(CONF_CLIENT_SECRET, DEFAULT_CLIENT_SECRET), + websession=aiohttp_client.async_get_clientsession(hass), + ) self._entry = entry self._hass = hass - async def _get_entry_token(self): + async def _get_entry_token(self) -> dict[str, Any]: # No token saved, generate one if ( CONF_TOKEN_EXPIRY not in self._entry.data @@ -119,13 +126,8 @@ async def _get_entry_token(self): async def _update_token(self): _LOGGER.debug("Fetching new access token") - token = await self.get_new_token( - username=self._entry.data[CONF_USERNAME], - password=self._entry.data[CONF_PASSWORD], - client_id=self._entry.data.get(CONF_CLIENT_ID, DEFAULT_CLIENT_ID), - client_secret=self._entry.data.get( - CONF_CLIENT_SECRET, DEFAULT_CLIENT_SECRET - ), + token = await super().get_new_token( + self._username, self._password, self._client_id, self._client_secret ) _LOGGER.debug("New token: %s", token) diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index 90bd6068ecb13..272295cd512e3 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -214,6 +214,18 @@ async def async_setup( self._options = options await self.hass.async_add_executor_job(self.setup) + device_registry = dr.async_get(self.hass) + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + configuration_url=f"http://{self.host}", + connections={(dr.CONNECTION_NETWORK_MAC, self.mac)}, + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="AVM", + model=self.model, + name=self.config_entry.title, + sw_version=self.current_firmware, + ) + def setup(self) -> None: """Set up FritzboxTools class.""" diff --git a/homeassistant/components/fritz/entity.py b/homeassistant/components/fritz/entity.py index 45665c786d46d..33eb60d72cf30 100644 --- a/homeassistant/components/fritz/entity.py +++ b/homeassistant/components/fritz/entity.py @@ -68,23 +68,14 @@ def __init__(self, avm_wrapper: AvmWrapper, device_name: str) -> None: """Init device info class.""" self._avm_wrapper = avm_wrapper self._device_name = device_name - - @property - def mac_address(self) -> str: - """Return the mac address of the main device.""" - return self._avm_wrapper.mac + self.mac_address = self._avm_wrapper.mac @property def device_info(self) -> DeviceInfo: """Return the device information.""" return DeviceInfo( - configuration_url=f"http://{self._avm_wrapper.host}", connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)}, identifiers={(DOMAIN, self._avm_wrapper.unique_id)}, - manufacturer="AVM", - model=self._avm_wrapper.model, - name=self._device_name, - sw_version=self._avm_wrapper.current_firmware, ) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2094f817dcd71..267374aa302c9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250103.0"] + "requirements": ["home-assistant-frontend==20250106.0"] } diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index b510569eb47f3..8d09c902f3667 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -114,6 +114,7 @@ async def async_update(self) -> None: self._attr_hs_color = color_util.color_RGB_to_hs(*rgb) self._attr_color_mode = ColorMode.HS else: + color_temp = self.device["status"].get("color_temp") self._attr_color_temp_kelvin = ( None if color_temp is None diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 33cae2315956e..09943faf0a2f2 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.63", "babel==2.15.0"] + "requirements": ["holidays==0.64", "babel==2.15.0"] } diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 818c4e6fe1938..d7c042c2a9171 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -168,7 +168,7 @@ async def _run_appliance_service[*_Ts]( error_translation_placeholders: dict[str, str], ) -> None: try: - await hass.async_add_executor_job(getattr(appliance, method), args) + await hass.async_add_executor_job(getattr(appliance, method), *args) except api.HomeConnectError as err: raise HomeAssistantError( translation_domain=DOMAIN, diff --git a/homeassistant/components/home_connect/select.py b/homeassistant/components/home_connect/select.py index c97b3db28e0c9..a4a5861afbe09 100644 --- a/homeassistant/components/home_connect/select.py +++ b/homeassistant/components/home_connect/select.py @@ -220,7 +220,7 @@ def get_entities() -> list[HomeConnectProgramSelectEntity]: with contextlib.suppress(HomeConnectError): programs = device.appliance.get_programs_available() if programs: - for program in programs: + for program in programs.copy(): if program not in PROGRAMS_TRANSLATION_KEYS_MAP: programs.remove(program) if program not in programs_not_found: diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 13bfc51255151..83937809b6048 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -12,6 +12,6 @@ "iot_class": "local_polling", "loggers": ["homewizard_energy"], "quality_scale": "platinum", - "requirements": ["python-homewizard-energy==v7.0.0"], + "requirements": ["python-homewizard-energy==v7.0.1"], "zeroconf": ["_hwenergy._tcp.local."] } diff --git a/homeassistant/components/iron_os/number.py b/homeassistant/components/iron_os/number.py index 583844223ddab..e50b227bbef84 100644 --- a/homeassistant/components/iron_os/number.py +++ b/homeassistant/components/iron_os/number.py @@ -188,8 +188,8 @@ def multiply(value: float | None, multiplier: float) -> float | None: characteristic=CharSetting.POWER_LIMIT, mode=NumberMode.BOX, native_min_value=0, - native_max_value=12, - native_step=0.1, + native_max_value=120, + native_step=5, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfPower.WATT, entity_registry_enabled_default=False, diff --git a/homeassistant/components/iron_os/strings.json b/homeassistant/components/iron_os/strings.json index 04c5528055057..967b966e44e40 100644 --- a/homeassistant/components/iron_os/strings.json +++ b/homeassistant/components/iron_os/strings.json @@ -128,8 +128,8 @@ "temp_unit": { "name": "Temperature display unit", "state": { - "celsius": "Celsius (C°)", - "fahrenheit": "Fahrenheit (F°)" + "celsius": "Celsius (°C)", + "fahrenheit": "Fahrenheit (°F)" } }, "desc_scroll_speed": { diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 5a066d015f22a..f66ffb0c6aeea 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -13,7 +13,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["demetriek"], - "requirements": ["demetriek==1.1.0"], + "requirements": ["demetriek==1.1.1"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:LaMetric:1" diff --git a/homeassistant/components/lametric/number.py b/homeassistant/components/lametric/number.py index 1025e04a4a8c1..a1d922c2d80d8 100644 --- a/homeassistant/components/lametric/number.py +++ b/homeassistant/components/lametric/number.py @@ -50,7 +50,7 @@ class LaMetricNumberEntityDescription(NumberEntityDescription): native_step=1, native_min_value=0, native_max_value=100, - has_fn=lambda device: bool(device.audio), + has_fn=lambda device: bool(device.audio and device.audio.available), value_fn=lambda device: device.audio.volume if device.audio else 0, set_value_fn=lambda api, volume: api.audio(volume=int(volume)), ), diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 2e16eb2082b67..9940ee15dca01 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -53,6 +53,6 @@ "requirements": [ "aiolifx==1.1.2", "aiolifx-effects==0.3.2", - "aiolifx-themes==0.5.5" + "aiolifx-themes==0.6.0" ] } diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 33bd259469b91..76fbea7032208 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -354,7 +354,7 @@ def filter_turn_off_params( if not params: return params - supported_features = light.supported_features + supported_features = light.supported_features_compat if LightEntityFeature.FLASH not in supported_features: params.pop(ATTR_FLASH, None) @@ -366,7 +366,7 @@ def filter_turn_off_params( def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[str, Any]: """Filter out params not supported by the light.""" - supported_features = light.supported_features + supported_features = light.supported_features_compat if LightEntityFeature.EFFECT not in supported_features: params.pop(ATTR_EFFECT, None) @@ -1093,7 +1093,7 @@ def effect(self) -> str | None: def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" data: dict[str, Any] = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat supported_color_modes = self._light_internal_supported_color_modes if ColorMode.COLOR_TEMP in supported_color_modes: @@ -1255,11 +1255,12 @@ def __validate_supported_color_modes( def state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" data: dict[str, Any] = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat supported_color_modes = self.supported_color_modes legacy_supported_color_modes = ( supported_color_modes or self._light_internal_supported_color_modes ) + supported_features_value = supported_features.value _is_on = self.is_on color_mode = self._light_internal_color_mode if _is_on else None @@ -1278,6 +1279,13 @@ def state_attributes(self) -> dict[str, Any] | None: data[ATTR_BRIGHTNESS] = self.brightness else: data[ATTR_BRIGHTNESS] = None + elif supported_features_value & _DEPRECATED_SUPPORT_BRIGHTNESS.value: + # Backwards compatibility for ambiguous / incomplete states + # Warning is printed by supported_features_compat, remove in 2025.1 + if _is_on: + data[ATTR_BRIGHTNESS] = self.brightness + else: + data[ATTR_BRIGHTNESS] = None if color_temp_supported(supported_color_modes): if color_mode == ColorMode.COLOR_TEMP: @@ -1292,6 +1300,21 @@ def state_attributes(self) -> dict[str, Any] | None: else: data[ATTR_COLOR_TEMP_KELVIN] = None data[_DEPRECATED_ATTR_COLOR_TEMP.value] = None + elif supported_features_value & _DEPRECATED_SUPPORT_COLOR_TEMP.value: + # Backwards compatibility + # Warning is printed by supported_features_compat, remove in 2025.1 + if _is_on: + color_temp_kelvin = self.color_temp_kelvin + data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin + if color_temp_kelvin: + data[_DEPRECATED_ATTR_COLOR_TEMP.value] = ( + color_util.color_temperature_kelvin_to_mired(color_temp_kelvin) + ) + else: + data[_DEPRECATED_ATTR_COLOR_TEMP.value] = None + else: + data[ATTR_COLOR_TEMP_KELVIN] = None + data[_DEPRECATED_ATTR_COLOR_TEMP.value] = None if color_supported(legacy_supported_color_modes) or color_temp_supported( legacy_supported_color_modes @@ -1329,7 +1352,24 @@ def _light_internal_supported_color_modes(self) -> set[ColorMode] | set[str]: type(self), report_issue, ) - return {ColorMode.ONOFF} + supported_features = self.supported_features_compat + supported_features_value = supported_features.value + supported_color_modes: set[ColorMode] = set() + + if supported_features_value & _DEPRECATED_SUPPORT_COLOR_TEMP.value: + supported_color_modes.add(ColorMode.COLOR_TEMP) + if supported_features_value & _DEPRECATED_SUPPORT_COLOR.value: + supported_color_modes.add(ColorMode.HS) + if ( + not supported_color_modes + and supported_features_value & _DEPRECATED_SUPPORT_BRIGHTNESS.value + ): + supported_color_modes = {ColorMode.BRIGHTNESS} + + if not supported_color_modes: + supported_color_modes = {ColorMode.ONOFF} + + return supported_color_modes @cached_property def supported_color_modes(self) -> set[ColorMode] | set[str] | None: @@ -1341,6 +1381,37 @@ def supported_features(self) -> LightEntityFeature: """Flag supported features.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> LightEntityFeature: + """Return the supported features as LightEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is not int: # noqa: E721 + return features + new_features = LightEntityFeature(features) + if self._deprecated_supported_features_reported is True: + return new_features + self._deprecated_supported_features_reported = True + report_issue = self._suggest_report_issue() + report_issue += ( + " and reference " + "https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation" + ) + _LOGGER.warning( + ( + "Entity %s (%s) is using deprecated supported features" + " values which will be removed in HA Core 2025.1. Instead it should use" + " %s and color modes, please %s" + ), + self.entity_id, + type(self), + repr(new_features), + report_issue, + ) + return new_features + def __should_report_light_issue(self) -> bool: """Return if light color mode issues should be reported.""" if not self.platform: diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json index adcdcd051376d..ef29601b831e2 100644 --- a/homeassistant/components/matter/icons.json +++ b/homeassistant/components/matter/icons.json @@ -57,6 +57,9 @@ }, "valve_position": { "default": "mdi:valve" + }, + "battery_replacement_description": { + "default": "mdi:battery-sync-outline" } } } diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index e7bbe1d19bd29..291b1ec1e2a1c 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -773,6 +773,19 @@ def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> MediaPlayerEntityFeature: + """Return the supported features as MediaPlayerEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = MediaPlayerEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + def turn_on(self) -> None: """Turn the media player on.""" raise NotImplementedError @@ -912,85 +925,87 @@ async def async_set_repeat(self, repeat: RepeatMode) -> None: @property def support_play(self) -> bool: """Boolean if play is supported.""" - return MediaPlayerEntityFeature.PLAY in self.supported_features + return MediaPlayerEntityFeature.PLAY in self.supported_features_compat @final @property def support_pause(self) -> bool: """Boolean if pause is supported.""" - return MediaPlayerEntityFeature.PAUSE in self.supported_features + return MediaPlayerEntityFeature.PAUSE in self.supported_features_compat @final @property def support_stop(self) -> bool: """Boolean if stop is supported.""" - return MediaPlayerEntityFeature.STOP in self.supported_features + return MediaPlayerEntityFeature.STOP in self.supported_features_compat @final @property def support_seek(self) -> bool: """Boolean if seek is supported.""" - return MediaPlayerEntityFeature.SEEK in self.supported_features + return MediaPlayerEntityFeature.SEEK in self.supported_features_compat @final @property def support_volume_set(self) -> bool: """Boolean if setting volume is supported.""" - return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features + return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat @final @property def support_volume_mute(self) -> bool: """Boolean if muting volume is supported.""" - return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features + return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features_compat @final @property def support_previous_track(self) -> bool: """Boolean if previous track command supported.""" - return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features + return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features_compat @final @property def support_next_track(self) -> bool: """Boolean if next track command supported.""" - return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features + return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features_compat @final @property def support_play_media(self) -> bool: """Boolean if play media command supported.""" - return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features + return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features_compat @final @property def support_select_source(self) -> bool: """Boolean if select source command supported.""" - return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features + return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features_compat @final @property def support_select_sound_mode(self) -> bool: """Boolean if select sound mode command supported.""" - return MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features + return ( + MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features_compat + ) @final @property def support_clear_playlist(self) -> bool: """Boolean if clear playlist command supported.""" - return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features + return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features_compat @final @property def support_shuffle_set(self) -> bool: """Boolean if shuffle is supported.""" - return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features + return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features_compat @final @property def support_grouping(self) -> bool: """Boolean if player grouping is supported.""" - return MediaPlayerEntityFeature.GROUPING in self.supported_features + return MediaPlayerEntityFeature.GROUPING in self.supported_features_compat async def async_toggle(self) -> None: """Toggle the power on the media player.""" @@ -1019,7 +1034,7 @@ async def async_volume_up(self) -> None: if ( self.volume_level is not None and self.volume_level < 1 - and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features + and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat ): await self.async_set_volume_level( min(1, self.volume_level + self.volume_step) @@ -1037,7 +1052,7 @@ async def async_volume_down(self) -> None: if ( self.volume_level is not None and self.volume_level > 0 - and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features + and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat ): await self.async_set_volume_level( max(0, self.volume_level - self.volume_step) @@ -1080,7 +1095,7 @@ def media_image_local(self) -> str | None: def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" data: dict[str, Any] = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat if ( source_list := self.source_list @@ -1286,7 +1301,7 @@ async def websocket_browse_media( connection.send_error(msg["id"], "entity_not_found", "Entity not found") return - if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features: + if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features_compat: connection.send_message( websocket_api.error_message( msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media" diff --git a/homeassistant/components/peblar/manifest.json b/homeassistant/components/peblar/manifest.json index 2c3e73ba76eb1..859682d3f1dab 100644 --- a/homeassistant/components/peblar/manifest.json +++ b/homeassistant/components/peblar/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["peblar==0.3.2"], + "requirements": ["peblar==0.3.3"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "pblr-*" }] } diff --git a/homeassistant/components/peblar/update.py b/homeassistant/components/peblar/update.py index 77879030f6c90..9e132da63bcc3 100644 --- a/homeassistant/components/peblar/update.py +++ b/homeassistant/components/peblar/update.py @@ -37,6 +37,7 @@ class PeblarUpdateEntityDescription(UpdateEntityDescription): key="firmware", device_class=UpdateDeviceClass.FIRMWARE, installed_fn=lambda x: x.current.firmware, + has_fn=lambda x: x.current.firmware is not None, available_fn=lambda x: x.available.firmware, ), PeblarUpdateEntityDescription( diff --git a/homeassistant/components/powerfox/coordinator.py b/homeassistant/components/powerfox/coordinator.py index f7ec5ab67161a..a4a26759b69b1 100644 --- a/homeassistant/components/powerfox/coordinator.py +++ b/homeassistant/components/powerfox/coordinator.py @@ -7,6 +7,7 @@ Powerfox, PowerfoxAuthenticationError, PowerfoxConnectionError, + PowerfoxNoDataError, Poweropti, ) @@ -45,5 +46,5 @@ async def _async_update_data(self) -> Poweropti: return await self.client.device(device_id=self.device.id) except PowerfoxAuthenticationError as err: raise ConfigEntryAuthFailed(err) from err - except PowerfoxConnectionError as err: + except (PowerfoxConnectionError, PowerfoxNoDataError) as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/powerfox/manifest.json b/homeassistant/components/powerfox/manifest.json index 7083ffe8de776..bb72d73b5a8c0 100644 --- a/homeassistant/components/powerfox/manifest.json +++ b/homeassistant/components/powerfox/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/powerfox", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["powerfox==1.0.0"], + "requirements": ["powerfox==1.2.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index af7732780293d..a45107181dee4 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -180,7 +180,7 @@ def guarded_import( # Allow import of _strptime needed by datetime.datetime.strptime if name == "_strptime": return __import__(name, globals, locals, fromlist, level) - raise ScriptError(f"Not allowed to import {name}") + raise ImportError(f"Not allowed to import {name}") def guarded_inplacevar(op: str, target: Any, operand: Any) -> Any: diff --git a/homeassistant/components/reolink/views.py b/homeassistant/components/reolink/views.py index 3b32ebaf74e5b..1a4585bc99768 100644 --- a/homeassistant/components/reolink/views.py +++ b/homeassistant/components/reolink/views.py @@ -2,9 +2,9 @@ from __future__ import annotations +from base64 import urlsafe_b64decode, urlsafe_b64encode from http import HTTPStatus import logging -from urllib import parse from aiohttp import ClientError, ClientTimeout, web from reolink_aio.enums import VodRequestType @@ -31,7 +31,7 @@ def async_generate_playback_proxy_url( return url_format.format( config_entry_id=config_entry_id, channel=channel, - filename=parse.quote(filename, safe=""), + filename=urlsafe_b64encode(filename.encode("utf-8")).decode("utf-8"), stream_res=stream_res, vod_type=vod_type, ) @@ -66,7 +66,7 @@ async def get( """Get playback proxy video response.""" retry = retry - 1 - filename = parse.unquote(filename) + filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8") ch = int(channel) try: host = get_host(self.hass, config_entry_id) @@ -77,7 +77,7 @@ async def get( try: mime_type, reolink_url = await host.api.get_vod_source( - ch, filename, stream_res, VodRequestType(vod_type) + ch, filename_decoded, stream_res, VodRequestType(vod_type) ) except ReolinkError as err: _LOGGER.warning("Reolink playback proxy error: %s", str(err)) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index d02dddece42f1..bc82aadffed12 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -9,7 +9,13 @@ import logging from typing import Any -from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials +from roborock import ( + HomeDataRoom, + RoborockException, + RoborockInvalidCredentials, + RoborockInvalidUserAgreement, + RoborockNoUserAgreement, +) from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 from roborock.version_a01_apis import RoborockMqttClientA01 @@ -60,12 +66,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> translation_domain=DOMAIN, translation_key="invalid_credentials", ) from err + except RoborockInvalidUserAgreement as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="invalid_user_agreement", + ) from err + except RoborockNoUserAgreement as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="no_user_agreement", + ) from err except RoborockException as err: raise ConfigEntryNotReady( "Failed to get Roborock home data", translation_domain=DOMAIN, translation_key="home_data_fail", ) from err + _LOGGER.debug("Got home data %s", home_data) all_devices: list[HomeDataDevice] = home_data.devices + home_data.received_devices device_map: dict[str, HomeDataDevice] = { diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py index 200614b024e2d..1a6b67286bb1c 100644 --- a/homeassistant/components/roborock/config_flow.py +++ b/homeassistant/components/roborock/config_flow.py @@ -60,7 +60,7 @@ async def async_step_user( if user_input is not None: username = user_input[CONF_USERNAME] await self.async_set_unique_id(username.lower()) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(error="already_configured_account") self._username = username _LOGGER.debug("Requesting code for Roborock account") self._client = RoborockApiClient(username) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 8ff82cae39316..8c66f6ab98683 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -28,7 +28,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, @@ -422,6 +422,12 @@ }, "update_options_failed": { "message": "Failed to update Roborock options" + }, + "invalid_user_agreement": { + "message": "User agreement must be accepted again. Open your Roborock app and accept the agreement." + }, + "no_user_agreement": { + "message": "You have not valid user agreement. Open your Roborock app and accept the agreement." } }, "services": { diff --git a/homeassistant/components/slide_local/config_flow.py b/homeassistant/components/slide_local/config_flow.py index 23c509a02dcfd..a4255f0769f7c 100644 --- a/homeassistant/components/slide_local/config_flow.py +++ b/homeassistant/components/slide_local/config_flow.py @@ -73,7 +73,6 @@ async def async_test_connection( return {} # API version 2 is not working, try API version 1 instead - await slide.slide_del(user_input[CONF_HOST]) await slide.slide_add( user_input[CONF_HOST], user_input.get(CONF_PASSWORD, ""), @@ -185,14 +184,15 @@ async def async_step_zeroconf( await self.async_set_unique_id(self._mac) - self._abort_if_unique_id_configured( - {CONF_HOST: discovery_info.host}, reload_on_update=True - ) + ip = str(discovery_info.ip_address) + _LOGGER.debug("Slide device discovered, ip %s", ip) + + self._abort_if_unique_id_configured({CONF_HOST: ip}, reload_on_update=True) errors = {} if errors := await self.async_test_connection( { - CONF_HOST: self._host, + CONF_HOST: ip, } ): return self.async_abort( @@ -202,7 +202,7 @@ async def async_step_zeroconf( }, ) - self._host = discovery_info.host + self._host = ip return await self.async_step_zeroconf_confirm() diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 631ace3792ffa..925f11e4c6540 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/solax", "iot_class": "local_polling", "loggers": ["solax"], - "requirements": ["solax==3.2.1"] + "requirements": ["solax==3.2.3"] } diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 4d1c98bc4fcf2..331bf383c7082 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -115,6 +115,7 @@ async def build_item_response( item_type = CONTENT_TYPE_TO_CHILD_TYPE[search_type] children = [] + list_playable = [] for item in result["items"]: item_id = str(item["id"]) item_thumbnail: str | None = None @@ -131,7 +132,7 @@ async def build_item_response( child_media_class = CONTENT_TYPE_MEDIA_CLASS[MediaType.ALBUM] can_expand = True can_play = True - elif item["hasitems"]: + elif item["hasitems"] and not item["isaudio"]: child_item_type = "Favorites" child_media_class = CONTENT_TYPE_MEDIA_CLASS["Favorites"] can_expand = True @@ -139,8 +140,8 @@ async def build_item_response( else: child_item_type = "Favorites" child_media_class = CONTENT_TYPE_MEDIA_CLASS[MediaType.TRACK] - can_expand = False - can_play = True + can_expand = item["hasitems"] + can_play = item["isaudio"] and item.get("url") if artwork_track_id := item.get("artwork_track_id"): if internal_request: @@ -166,6 +167,7 @@ async def build_item_response( thumbnail=item_thumbnail, ) ) + list_playable.append(can_play) if children is None: raise BrowseError(f"Media not found: {search_type} / {search_id}") @@ -179,7 +181,7 @@ async def build_item_response( children_media_class=media_class["children"], media_content_id=search_id, media_content_type=search_type, - can_play=search_type != "Favorites", + can_play=any(list_playable), children=children, can_expand=True, ) diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index f39411e8afa49..176b059f3d522 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pysuez", "regex"], "quality_scale": "bronze", - "requirements": ["pysuezV2==1.3.5"] + "requirements": ["pysuezV2==2.0.1"] } diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 7797f0a36a316..a975e675ceb55 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -300,5 +300,5 @@ "documentation": "https://www.home-assistant.io/integrations/tplink", "iot_class": "local_polling", "loggers": ["kasa"], - "requirements": ["python-kasa[speedups]==0.9.0"] + "requirements": ["python-kasa[speedups]==0.9.1"] } diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index 664d52c16af7e..c0aef09e8c324 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -21,7 +21,7 @@ }, "user_auth_confirm": { "title": "Authenticate", - "description": "The device requires authentication, please input your TP-Link credentials below.", + "description": "The device requires authentication, please input your TP-Link credentials below. Note, that both e-mail and password are case-sensitive.", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index c04c5492a403f..b1cb98dbca6d8 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["twentemilieu"], "quality_scale": "silver", - "requirements": ["twentemilieu==2.2.0"] + "requirements": ["twentemilieu==2.2.1"] } diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 1226f96c25363..018a600f03749 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.1.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.4.1", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 46e35bb3e1108..6fe2c3e2a5b3d 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -312,7 +312,7 @@ def battery_icon(self) -> str: @property def capability_attributes(self) -> dict[str, Any] | None: """Return capability attributes.""" - if VacuumEntityFeature.FAN_SPEED in self.supported_features: + if VacuumEntityFeature.FAN_SPEED in self.supported_features_compat: return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} return None @@ -330,7 +330,7 @@ def fan_speed_list(self) -> list[str]: def state_attributes(self) -> dict[str, Any]: """Return the state attributes of the vacuum cleaner.""" data: dict[str, Any] = {} - supported_features = self.supported_features + supported_features = self.supported_features_compat if VacuumEntityFeature.BATTERY in supported_features: data[ATTR_BATTERY_LEVEL] = self.battery_level @@ -369,6 +369,19 @@ def supported_features(self) -> VacuumEntityFeature: """Flag vacuum cleaner features that are supported.""" return self._attr_supported_features + @property + def supported_features_compat(self) -> VacuumEntityFeature: + """Return the supported features as VacuumEntityFeature. + + Remove this compatibility shim in 2025.1 or later. + """ + features = self.supported_features + if type(features) is int: # noqa: E721 + new_features = VacuumEntityFeature(features) + self._report_deprecated_supported_features_values(new_features) + return new_features + return features + def stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" raise NotImplementedError diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json index cca1789bf7e54..8f8de694b2dcb 100644 --- a/homeassistant/components/waze_travel_time/strings.json +++ b/homeassistant/components/waze_travel_time/strings.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "description": "For Origin and Destination, enter the address or the GPS coordinates of the location (GPS coordinates has to be separated by a comma). You can also enter an entity id which provides this information in its state, an entity id with latitude and longitude attributes, or zone friendly name.", + "description": "For Origin and Destination, enter the address or the GPS coordinates of the location (GPS coordinates has to be separated by a comma). You can also enter an entity ID which provides this information in its state, an entity ID with latitude and longitude attributes, or zone friendly name.", "data": { "name": "[%key:common::config_flow::data::name%]", "origin": "Origin", @@ -26,13 +26,13 @@ "description": "Some options will allow you to force the integration to use a particular route or avoid a particular route in its time travel calculation.", "data": { "units": "Units", - "vehicle_type": "Vehicle Type", + "vehicle_type": "Vehicle type", "incl_filter": "Exact streetname which must be part of the selected route", "excl_filter": "Exact streetname which must NOT be part of the selected route", - "realtime": "Realtime Travel Time?", - "avoid_toll_roads": "Avoid Toll Roads?", - "avoid_ferries": "Avoid Ferries?", - "avoid_subscription_roads": "Avoid Roads Needing a Vignette / Subscription?" + "realtime": "Realtime travel time?", + "avoid_toll_roads": "Avoid toll roads?", + "avoid_ferries": "Avoid ferries?", + "avoid_subscription_roads": "Avoid roads needing a vignette / subscription?" } } } @@ -47,8 +47,8 @@ }, "units": { "options": { - "metric": "Metric System", - "imperial": "Imperial System" + "metric": "Metric system", + "imperial": "Imperial system" } }, "region": { @@ -63,8 +63,8 @@ }, "services": { "get_travel_times": { - "name": "Get Travel Times", - "description": "Get route alternatives and travel times between two locations.", + "name": "Get travel times", + "description": "Retrieves route alternatives and travel times between two locations.", "fields": { "origin": { "name": "[%key:component::waze_travel_time::config::step::user::data::origin%]", @@ -76,7 +76,7 @@ }, "region": { "name": "[%key:component::waze_travel_time::config::step::user::data::region%]", - "description": "The region. Controls which waze server is used." + "description": "The region. Controls which Waze server is used." }, "units": { "name": "[%key:component::waze_travel_time::options::step::init::data::units%]", diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index de9cbe694d899..bb5e6333b8b7a 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.63"] + "requirements": ["holidays==0.64"] } diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 45d8f6bb25f9e..975a1804853c6 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.25", "zha==0.0.44"], + "requirements": ["universal-silabs-flasher==0.0.25", "zha==0.0.45"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/const.py b/homeassistant/const.py index 5a088d364497d..e641ae4254c3c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 91845cdf5214d..19076c4edc000 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -7,7 +7,7 @@ from collections import deque from collections.abc import Callable, Coroutine, Iterable, Mapping import dataclasses -from enum import Enum, auto +from enum import Enum, IntFlag, auto import functools as ft import logging import math @@ -1639,6 +1639,31 @@ def _suggest_report_issue(self) -> str: self.hass, integration_domain=platform_name, module=type(self).__module__ ) + @callback + def _report_deprecated_supported_features_values( + self, replacement: IntFlag + ) -> None: + """Report deprecated supported features values.""" + if self._deprecated_supported_features_reported is True: + return + self._deprecated_supported_features_reported = True + report_issue = self._suggest_report_issue() + report_issue += ( + " and reference " + "https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation" + ) + _LOGGER.warning( + ( + "Entity %s (%s) is using deprecated supported features" + " values which will be removed in HA Core 2025.1. Instead it should use" + " %s, please %s" + ), + self.entity_id, + type(self), + repr(replacement), + report_issue, + ) + class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True): """A class that describes toggle entities.""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b07909e08eb91..dac77fd4276da 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,11 +31,11 @@ dbus-fast==2.24.3 fnv-hash-fast==1.0.2 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 -habluetooth==3.6.0 +habluetooth==3.7.0 hass-nabucasa==0.87.0 hassil==2.1.0 home-assistant-bluetooth==1.13.0 -home-assistant-frontend==20250103.0 +home-assistant-frontend==20250106.0 home-assistant-intents==2025.1.1 httpx==0.27.2 ifaddr==0.2.0 diff --git a/pyproject.toml b/pyproject.toml index c87e499155c31..f94d54feb8852 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.1.0" +version = "2025.1.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index 36025003d9d98..e64a48cbb8124 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ aiokef==0.2.16 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.5.5 +aiolifx-themes==0.6.0 # homeassistant.components.lifx aiolifx==1.1.2 @@ -585,7 +585,7 @@ bizkaibus==0.1.1 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==1.1.0 +bleak-esphome==2.0.0 # homeassistant.components.bluetooth bleak-retry-connector==3.6.0 @@ -704,7 +704,7 @@ connect-box==0.3.1 construct==2.10.68 # homeassistant.components.cookidoo -cookidoo-api==0.10.0 +cookidoo-api==0.11.2 # homeassistant.components.backup # homeassistant.components.utility_meter @@ -749,7 +749,7 @@ defusedxml==0.7.1 deluge-client==1.10.2 # homeassistant.components.lametric -demetriek==1.1.0 +demetriek==1.1.1 # homeassistant.components.denonavr denonavr==1.0.1 @@ -1091,7 +1091,7 @@ ha-philipsjs==3.2.2 habitipy==0.3.3 # homeassistant.components.bluetooth -habluetooth==3.6.0 +habluetooth==3.7.0 # homeassistant.components.cloud hass-nabucasa==0.87.0 @@ -1131,10 +1131,10 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.63 +holidays==0.64 # homeassistant.components.frontend -home-assistant-frontend==20250103.0 +home-assistant-frontend==20250106.0 # homeassistant.components.conversation home-assistant-intents==2025.1.1 @@ -1561,7 +1561,7 @@ openhomedevice==2.2.0 opensensemap-api==0.2.0 # homeassistant.components.enigma2 -openwebifpy==4.3.0 +openwebifpy==4.3.1 # homeassistant.components.luci openwrt-luci-rpc==1.1.17 @@ -1603,7 +1603,7 @@ panasonic-viera==0.4.2 pdunehd==1.3.2 # homeassistant.components.peblar -peblar==0.3.2 +peblar==0.3.3 # homeassistant.components.peco peco==0.0.30 @@ -1650,7 +1650,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.powerfox -powerfox==1.0.0 +powerfox==1.2.0 # homeassistant.components.reddit praw==7.5.0 @@ -2309,7 +2309,7 @@ pysqueezebox==0.10.0 pystiebeleltron==0.0.1.dev2 # homeassistant.components.suez_water -pysuezV2==1.3.5 +pysuezV2==2.0.1 # homeassistant.components.switchbee pyswitchbee==1.8.3 @@ -2363,7 +2363,7 @@ python-gitlab==1.6.0 python-homeassistant-analytics==0.8.1 # homeassistant.components.homewizard -python-homewizard-energy==v7.0.0 +python-homewizard-energy==v7.0.1 # homeassistant.components.hp_ilo python-hpilo==4.4.3 @@ -2378,7 +2378,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.9.0 +python-kasa[speedups]==0.9.1 # homeassistant.components.linkplay python-linkplay==0.1.1 @@ -2720,7 +2720,7 @@ solaredge-local==0.2.3 solarlog_cli==0.4.0 # homeassistant.components.solax -solax==3.2.1 +solax==3.2.3 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 @@ -2895,7 +2895,7 @@ ttn_client==1.2.0 tuya-device-sharing-sdk==0.2.1 # homeassistant.components.twentemilieu -twentemilieu==2.2.0 +twentemilieu==2.2.1 # homeassistant.components.twilio twilio==6.32.0 @@ -2910,7 +2910,7 @@ typedmonarchmoney==0.3.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.1.0 +uiprotect==7.4.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 @@ -3100,7 +3100,7 @@ zeroconf==0.136.2 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.44 +zha==0.0.45 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03e594dcf53d6..bf0bcb7f9d307 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -264,7 +264,7 @@ aiokafka==0.10.0 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.5.5 +aiolifx-themes==0.6.0 # homeassistant.components.lifx aiolifx==1.1.2 @@ -516,7 +516,7 @@ bimmer-connected[china]==0.17.2 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==1.1.0 +bleak-esphome==2.0.0 # homeassistant.components.bluetooth bleak-retry-connector==3.6.0 @@ -600,7 +600,7 @@ colorthief==0.2.1 construct==2.10.68 # homeassistant.components.cookidoo -cookidoo-api==0.10.0 +cookidoo-api==0.11.2 # homeassistant.components.backup # homeassistant.components.utility_meter @@ -639,7 +639,7 @@ defusedxml==0.7.1 deluge-client==1.10.2 # homeassistant.components.lametric -demetriek==1.1.0 +demetriek==1.1.1 # homeassistant.components.denonavr denonavr==1.0.1 @@ -932,7 +932,7 @@ ha-philipsjs==3.2.2 habitipy==0.3.3 # homeassistant.components.bluetooth -habluetooth==3.6.0 +habluetooth==3.7.0 # homeassistant.components.cloud hass-nabucasa==0.87.0 @@ -960,10 +960,10 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.63 +holidays==0.64 # homeassistant.components.frontend -home-assistant-frontend==20250103.0 +home-assistant-frontend==20250106.0 # homeassistant.components.conversation home-assistant-intents==2025.1.1 @@ -1303,7 +1303,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.enigma2 -openwebifpy==4.3.0 +openwebifpy==4.3.1 # homeassistant.components.opower opower==0.8.7 @@ -1330,7 +1330,7 @@ panasonic-viera==0.4.2 pdunehd==1.3.2 # homeassistant.components.peblar -peblar==0.3.2 +peblar==0.3.3 # homeassistant.components.peco peco==0.0.30 @@ -1360,7 +1360,7 @@ plumlightpad==0.0.11 poolsense==0.0.8 # homeassistant.components.powerfox -powerfox==1.0.0 +powerfox==1.2.0 # homeassistant.components.reddit praw==7.5.0 @@ -1875,7 +1875,7 @@ pyspeex-noise==1.0.2 pysqueezebox==0.10.0 # homeassistant.components.suez_water -pysuezV2==1.3.5 +pysuezV2==2.0.1 # homeassistant.components.switchbee pyswitchbee==1.8.3 @@ -1905,7 +1905,7 @@ python-fullykiosk==0.0.14 python-homeassistant-analytics==0.8.1 # homeassistant.components.homewizard -python-homewizard-energy==v7.0.0 +python-homewizard-energy==v7.0.1 # homeassistant.components.izone python-izone==1.2.9 @@ -1914,7 +1914,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.9.0 +python-kasa[speedups]==0.9.1 # homeassistant.components.linkplay python-linkplay==0.1.1 @@ -2181,7 +2181,7 @@ soco==0.30.6 solarlog_cli==0.4.0 # homeassistant.components.solax -solax==3.2.1 +solax==3.2.3 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 @@ -2317,7 +2317,7 @@ ttn_client==1.2.0 tuya-device-sharing-sdk==0.2.1 # homeassistant.components.twentemilieu -twentemilieu==2.2.0 +twentemilieu==2.2.1 # homeassistant.components.twilio twilio==6.32.0 @@ -2332,7 +2332,7 @@ typedmonarchmoney==0.3.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.1.0 +uiprotect==7.4.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 @@ -2489,7 +2489,7 @@ zeroconf==0.136.2 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.44 +zha==0.0.45 # homeassistant.components.zwave_js zwave-js-server-python==0.60.0 diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index a3045e27cf1db..32520fcad23cb 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -826,6 +826,26 @@ def test_deprecated_state_constants( import_and_test_deprecated_constant_enum(caplog, module, enum, "STATE_", "2025.10") +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockCamera(camera.Camera): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockCamera() + assert entity.supported_features_compat is camera.CameraEntityFeature(1) + assert "MockCamera" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "CameraEntityFeature.ON_OFF" in caplog.text + caplog.clear() + assert entity.supported_features_compat is camera.CameraEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text + + @pytest.mark.usefixtures("mock_camera") async def test_entity_picture_url_changes_on_token_update(hass: HomeAssistant) -> None: """Test the token is rotated and entity entity picture cache is cleared.""" diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index e43b64b16a79a..646c44e4ac287 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -2,6 +2,8 @@ from enum import Enum +import pytest + from homeassistant.components import cover from homeassistant.components.cover import CoverState from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, SERVICE_TOGGLE @@ -153,3 +155,20 @@ def _create_tuples(enum: type[Enum], constant_prefix: str) -> list[tuple[Enum, s def test_all() -> None: """Test module.__all__ is correctly set.""" help_test_all(cover) + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockCoverEntity(cover.CoverEntity): + _attr_supported_features = 1 + + entity = MockCoverEntity() + assert entity.supported_features is cover.CoverEntityFeature(1) + assert "MockCoverEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "CoverEntityFeature.OPEN" in caplog.text + caplog.clear() + assert entity.supported_features is cover.CoverEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text diff --git a/tests/components/esphome/bluetooth/test_client.py b/tests/components/esphome/bluetooth/test_client.py index 98993be37d058..77d315f096d36 100644 --- a/tests/components/esphome/bluetooth/test_client.py +++ b/tests/components/esphome/bluetooth/test_client.py @@ -4,7 +4,6 @@ from aioesphomeapi import APIClient, APIVersion, BluetoothProxyFeature, DeviceInfo from bleak.exc import BleakError -from bleak_esphome.backend.cache import ESPHomeBluetoothCache from bleak_esphome.backend.client import ESPHomeClient, ESPHomeClientData from bleak_esphome.backend.device import ESPHomeBluetoothDevice from bleak_esphome.backend.scanner import ESPHomeScanner @@ -27,7 +26,6 @@ async def client_data_fixture( connector = HaBluetoothConnector(ESPHomeClientData, ESP_MAC_ADDRESS, lambda: True) return ESPHomeClientData( bluetooth_device=ESPHomeBluetoothDevice(ESP_NAME, ESP_MAC_ADDRESS), - cache=ESPHomeBluetoothCache(), client=mock_client, device_info=DeviceInfo( mac_address=ESP_MAC_ADDRESS, diff --git a/tests/components/home_connect/test_select.py b/tests/components/home_connect/test_select.py index 7d5843e952524..af97597919627 100644 --- a/tests/components/home_connect/test_select.py +++ b/tests/components/home_connect/test_select.py @@ -10,11 +10,16 @@ BSH_ACTIVE_PROGRAM, BSH_SELECTED_PROGRAM, ) -from homeassistant.components.select import ATTR_OPTION, DOMAIN as SELECT_DOMAIN +from homeassistant.components.select import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SELECT_OPTION, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from .conftest import get_all_appliances @@ -52,6 +57,40 @@ async def test_select( assert config_entry.state is ConfigEntryState.LOADED +async def test_filter_unknown_programs( + bypass_throttle: Generator[None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: Mock, + appliance: Mock, + entity_registry: er.EntityRegistry, +) -> None: + """Test select that programs that are not part of the official Home Connect API specification are filtered out. + + We use two programs to ensure that programs are iterated over a copy of the list, + and it does not raise problems when removing an element from the original list. + """ + appliance.status.update(SETTINGS_STATUS) + appliance.get_programs_available.return_value = [ + PROGRAM, + "NonOfficialProgram", + "AntotherNonOfficialProgram", + ] + get_appliances.return_value = [appliance] + + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state is ConfigEntryState.LOADED + + entity = entity_registry.async_get("select.washer_selected_program") + assert entity + assert entity.capabilities.get(ATTR_OPTIONS) == [ + "dishcare_dishwasher_program_eco_50" + ] + + @pytest.mark.parametrize( ("entity_id", "status", "program_to_set"), [ diff --git a/tests/components/iron_os/snapshots/test_number.ambr b/tests/components/iron_os/snapshots/test_number.ambr index 24663cc4b0fa5..fc4fe96d74637 100644 --- a/tests/components/iron_os/snapshots/test_number.ambr +++ b/tests/components/iron_os/snapshots/test_number.ambr @@ -620,10 +620,10 @@ }), 'area_id': None, 'capabilities': dict({ - 'max': 12, + 'max': 120, 'min': 0, 'mode': , - 'step': 0.1, + 'step': 5, }), 'config_entry_id': , 'device_class': None, @@ -656,10 +656,10 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Pinecil Power limit', - 'max': 12, + 'max': 120, 'min': 0, 'mode': , - 'step': 0.1, + 'step': 5, 'unit_of_measurement': , }), 'context': , diff --git a/tests/components/iron_os/test_number.py b/tests/components/iron_os/test_number.py index 088b66feb6475..bdec922a88ccd 100644 --- a/tests/components/iron_os/test_number.py +++ b/tests/components/iron_os/test_number.py @@ -126,7 +126,7 @@ async def test_state( 2.0, 2.0, ), - ("number.pinecil_power_limit", CharSetting.POWER_LIMIT, 12.0, 12.0), + ("number.pinecil_power_limit", CharSetting.POWER_LIMIT, 120, 120), ("number.pinecil_quick_charge_voltage", CharSetting.QC_IDEAL_VOLTAGE, 9.0, 9.0), ( "number.pinecil_short_press_temperature_step", diff --git a/tests/components/lametric/snapshots/test_diagnostics.ambr b/tests/components/lametric/snapshots/test_diagnostics.ambr index 7517cfe035e8d..8b8f98b5806b3 100644 --- a/tests/components/lametric/snapshots/test_diagnostics.ambr +++ b/tests/components/lametric/snapshots/test_diagnostics.ambr @@ -2,6 +2,7 @@ # name: test_diagnostics dict({ 'audio': dict({ + 'available': True, 'volume': 100, 'volume_limit': dict({ 'range_max': 100, diff --git a/tests/components/light/common.py b/tests/components/light/common.py index b29ac0c7c8922..77411cd637d93 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -26,7 +26,6 @@ DOMAIN, ColorMode, LightEntity, - LightEntityFeature, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -157,7 +156,7 @@ class MockLight(MockToggleEntity, LightEntity): _attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN _attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN - supported_features = LightEntityFeature(0) + supported_features = 0 brightness = None color_temp_kelvin = None diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 303bf68f68c68..776995ee52387 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,6 +1,7 @@ """The tests for the Light component.""" from types import ModuleType +from typing import Literal from unittest.mock import MagicMock, mock_open, patch import pytest @@ -137,8 +138,13 @@ async def test_services( ent3.supported_color_modes = [light.ColorMode.HS] ent1.supported_features = light.LightEntityFeature.TRANSITION ent2.supported_features = ( - light.LightEntityFeature.EFFECT | light.LightEntityFeature.TRANSITION + light.SUPPORT_COLOR + | light.LightEntityFeature.EFFECT + | light.LightEntityFeature.TRANSITION ) + # Set color modes to none to trigger backwards compatibility in LightEntity + ent2.supported_color_modes = None + ent2.color_mode = None ent3.supported_features = ( light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION ) @@ -254,7 +260,10 @@ async def test_services( } _, data = ent2.last_call("turn_on") - assert data == {light.ATTR_EFFECT: "fun_effect"} + assert data == { + light.ATTR_EFFECT: "fun_effect", + light.ATTR_HS_COLOR: (0, 0), + } _, data = ent3.last_call("turn_on") assert data == {light.ATTR_FLASH: "short", light.ATTR_HS_COLOR: (71.059, 100)} @@ -338,6 +347,8 @@ async def test_services( _, data = ent2.last_call("turn_on") assert data == { + light.ATTR_BRIGHTNESS: 100, + light.ATTR_HS_COLOR: profile.hs_color, light.ATTR_TRANSITION: 1, } @@ -915,12 +926,16 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None: setup_test_component_platform(hass, light.DOMAIN, entities) entity0 = entities[0] - entity0.supported_color_modes = {light.ColorMode.BRIGHTNESS} - entity0.color_mode = light.ColorMode.BRIGHTNESS + entity0.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity0.supported_color_modes = None + entity0.color_mode = None entity0.brightness = 100 entity1 = entities[1] - entity1.supported_color_modes = {light.ColorMode.BRIGHTNESS} - entity1.color_mode = light.ColorMode.BRIGHTNESS + entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity1.brightness = 50 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -981,8 +996,10 @@ async def test_light_brightness_pct_conversion( setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) entity = mock_light_entities[0] - entity.supported_color_modes = {light.ColorMode.BRIGHTNESS} - entity.color_mode = light.ColorMode.BRIGHTNESS + entity.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity.supported_color_modes = None + entity.color_mode = None entity.brightness = 100 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1131,6 +1148,167 @@ async def test_profile_load_optional_hs_color(hass: HomeAssistant) -> None: assert invalid_profile_name not in profiles.data +@pytest.mark.parametrize("light_state", [STATE_ON, STATE_OFF]) +async def test_light_backwards_compatibility_supported_color_modes( + hass: HomeAssistant, light_state: Literal["on", "off"] +) -> None: + """Test supported_color_modes if not implemented by the entity.""" + entities = [ + MockLight("Test_0", light_state), + MockLight("Test_1", light_state), + MockLight("Test_2", light_state), + MockLight("Test_3", light_state), + MockLight("Test_4", light_state), + ] + + entity0 = entities[0] + + entity1 = entities[1] + entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None + + entity2 = entities[2] + entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None + + entity3 = entities[3] + entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None + + entity4 = entities[4] + entity4.supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP + ) + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None + + setup_test_component_platform(hass, light.DOMAIN, entities) + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.ONOFF] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.ONOFF + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.BRIGHTNESS] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN + + state = hass.states.get(entity2.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN + + state = hass.states.get(entity3.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.HS] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN + + state = hass.states.get(entity4.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.ColorMode.COLOR_TEMP, + light.ColorMode.HS, + ] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN + + +async def test_light_backwards_compatibility_color_mode(hass: HomeAssistant) -> None: + """Test color_mode if not implemented by the entity.""" + entities = [ + MockLight("Test_0", STATE_ON), + MockLight("Test_1", STATE_ON), + MockLight("Test_2", STATE_ON), + MockLight("Test_3", STATE_ON), + MockLight("Test_4", STATE_ON), + ] + + entity0 = entities[0] + + entity1 = entities[1] + entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None + entity1.brightness = 100 + + entity2 = entities[2] + entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None + entity2.color_temp_kelvin = 10000 + + entity3 = entities[3] + entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None + entity3.hs_color = (240, 100) + + entity4 = entities[4] + entity4.supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP + ) + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None + entity4.hs_color = (240, 100) + entity4.color_temp_kelvin = 10000 + + setup_test_component_platform(hass, light.DOMAIN, entities) + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.ONOFF] + assert state.attributes["color_mode"] == light.ColorMode.ONOFF + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.BRIGHTNESS] + assert state.attributes["color_mode"] == light.ColorMode.BRIGHTNESS + + state = hass.states.get(entity2.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP] + assert state.attributes["color_mode"] == light.ColorMode.COLOR_TEMP + assert state.attributes["rgb_color"] == (202, 218, 255) + assert state.attributes["hs_color"] == (221.575, 20.9) + assert state.attributes["xy_color"] == (0.278, 0.287) + + state = hass.states.get(entity3.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.HS] + assert state.attributes["color_mode"] == light.ColorMode.HS + + state = hass.states.get(entity4.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.ColorMode.COLOR_TEMP, + light.ColorMode.HS, + ] + # hs color prioritized over color_temp, light should report mode ColorMode.HS + assert state.attributes["color_mode"] == light.ColorMode.HS + + async def test_light_service_call_rgbw(hass: HomeAssistant) -> None: """Test rgbw functionality in service calls.""" entity0 = MockLight("Test_rgbw", STATE_ON) @@ -1186,7 +1364,7 @@ async def test_light_state_off(hass: HomeAssistant) -> None: "color_mode": None, "friendly_name": "Test_onoff", "supported_color_modes": [light.ColorMode.ONOFF], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, } state = hass.states.get(entity1.entity_id) @@ -1194,7 +1372,7 @@ async def test_light_state_off(hass: HomeAssistant) -> None: "color_mode": None, "friendly_name": "Test_brightness", "supported_color_modes": [light.ColorMode.BRIGHTNESS], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "brightness": None, } @@ -1203,7 +1381,7 @@ async def test_light_state_off(hass: HomeAssistant) -> None: "color_mode": None, "friendly_name": "Test_ct", "supported_color_modes": [light.ColorMode.COLOR_TEMP], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "brightness": None, "color_temp": None, "color_temp_kelvin": None, @@ -1221,7 +1399,7 @@ async def test_light_state_off(hass: HomeAssistant) -> None: "color_mode": None, "friendly_name": "Test_rgbw", "supported_color_modes": [light.ColorMode.RGBW], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "brightness": None, "rgbw_color": None, "hs_color": None, @@ -1252,7 +1430,7 @@ async def test_light_state_rgbw(hass: HomeAssistant) -> None: "color_mode": light.ColorMode.RGBW, "friendly_name": "Test_rgbw", "supported_color_modes": [light.ColorMode.RGBW], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "hs_color": (240.0, 25.0), "rgb_color": (3, 3, 4), "rgbw_color": (1, 2, 3, 4), @@ -1283,7 +1461,7 @@ async def test_light_state_rgbww(hass: HomeAssistant) -> None: "color_mode": light.ColorMode.RGBWW, "friendly_name": "Test_rgbww", "supported_color_modes": [light.ColorMode.RGBWW], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "hs_color": (60.0, 20.0), "rgb_color": (5, 5, 4), "rgbww_color": (1, 2, 3, 4, 5), @@ -1299,6 +1477,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: MockLight("Test_rgb", STATE_ON), MockLight("Test_xy", STATE_ON), MockLight("Test_all", STATE_ON), + MockLight("Test_legacy", STATE_ON), MockLight("Test_rgbw", STATE_ON), MockLight("Test_rgbww", STATE_ON), MockLight("Test_temperature", STATE_ON), @@ -1322,13 +1501,19 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: } entity4 = entities[4] - entity4.supported_color_modes = {light.ColorMode.RGBW} + entity4.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity5 = entities[5] - entity5.supported_color_modes = {light.ColorMode.RGBWW} + entity5.supported_color_modes = {light.ColorMode.RGBW} entity6 = entities[6] - entity6.supported_color_modes = {light.ColorMode.COLOR_TEMP} + entity6.supported_color_modes = {light.ColorMode.RGBWW} + + entity7 = entities[7] + entity7.supported_color_modes = {light.ColorMode.COLOR_TEMP} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1350,12 +1535,15 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: ] state = hass.states.get(entity4.entity_id) - assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBW] + assert state.attributes["supported_color_modes"] == [light.ColorMode.HS] state = hass.states.get(entity5.entity_id) - assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBWW] + assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBW] state = hass.states.get(entity6.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBWW] + + state = hass.states.get(entity7.entity_id) assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP] await hass.services.async_call( @@ -1370,6 +1558,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 100, "hs_color": (240, 100), @@ -1385,10 +1574,12 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 255, "rgbw_color": (0, 0, 255, 0)} + assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} _, data = entity5.last_call("turn_on") - assert data == {"brightness": 255, "rgbww_color": (0, 0, 255, 0, 0)} + assert data == {"brightness": 255, "rgbw_color": (0, 0, 255, 0)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 255, "rgbww_color": (0, 0, 255, 0, 0)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 255, "color_temp_kelvin": 1739, "color_temp": 575} await hass.services.async_call( @@ -1403,6 +1594,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 100, "hs_color": (240, 0), @@ -1418,11 +1610,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 255, "hs_color": (240.0, 0.0)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 255, "rgbw_color": (0, 0, 0, 255)} + assert data == {"brightness": 255, "hs_color": (240.0, 0.0)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 255, "rgbw_color": (0, 0, 0, 255)} + _, data = entity6.last_call("turn_on") # The midpoint of the white channels is warm, compensated by adding green + blue assert data == {"brightness": 255, "rgbww_color": (0, 76, 141, 255, 255)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 255, "color_temp_kelvin": 5962, "color_temp": 167} await hass.services.async_call( @@ -1437,6 +1631,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgb_color": (128, 0, 0), @@ -1451,12 +1646,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: assert data == {"brightness": 128, "xy_color": (0.701, 0.299)} _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (128, 0, 0)} - _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 0)} + assert data == {"brightness": 128, "hs_color": (0.0, 100.0)} _, data = entity5.last_call("turn_on") - assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 0, 0)} + assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 0)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 0, 0)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 6279, "color_temp": 159} await hass.services.async_call( @@ -1471,6 +1667,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgb_color": (255, 255, 255), @@ -1486,11 +1683,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (255, 255, 255)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (0, 0, 0, 255)} + assert data == {"brightness": 128, "hs_color": (0.0, 0.0)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (0, 0, 0, 255)} + _, data = entity6.last_call("turn_on") # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167} await hass.services.async_call( @@ -1505,6 +1704,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "xy_color": (0.1, 0.8), @@ -1520,10 +1720,12 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "xy_color": (0.1, 0.8)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (0, 255, 22, 0)} + assert data == {"brightness": 128, "hs_color": (125.176, 100.0)} _, data = entity5.last_call("turn_on") - assert data == {"brightness": 128, "rgbww_color": (0, 255, 22, 0, 0)} + assert data == {"brightness": 128, "rgbw_color": (0, 255, 22, 0)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (0, 255, 22, 0, 0)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 8645, "color_temp": 115} await hass.services.async_call( @@ -1538,6 +1740,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "xy_color": (0.323, 0.329), @@ -1553,11 +1756,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "xy_color": (0.323, 0.329)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (1, 0, 0, 255)} + assert data == {"brightness": 128, "hs_color": (0.0, 0.392)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (1, 0, 0, 255)} + _, data = entity6.last_call("turn_on") # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167} await hass.services.async_call( @@ -1572,6 +1777,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgbw_color": (128, 0, 0, 64), @@ -1587,11 +1793,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (128, 43, 43)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 64)} + assert data == {"brightness": 128, "hs_color": (0.0, 66.406)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 64)} + _, data = entity6.last_call("turn_on") # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (128, 0, 30, 117, 117)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 3011, "color_temp": 332} await hass.services.async_call( @@ -1606,6 +1814,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgbw_color": (255, 255, 255, 255), @@ -1621,11 +1830,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (255, 255, 255)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (255, 255, 255, 255)} + assert data == {"brightness": 128, "hs_color": (0.0, 0.0)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (255, 255, 255, 255)} + _, data = entity6.last_call("turn_on") # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167} await hass.services.async_call( @@ -1640,6 +1851,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgbww_color": (128, 0, 0, 64, 32), @@ -1655,10 +1867,12 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (128, 33, 26)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (128, 9, 0, 33)} + assert data == {"brightness": 128, "hs_color": (4.118, 79.688)} _, data = entity5.last_call("turn_on") - assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 64, 32)} + assert data == {"brightness": 128, "rgbw_color": (128, 9, 0, 33)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 64, 32)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 3845, "color_temp": 260} await hass.services.async_call( @@ -1673,6 +1887,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgbww_color": (255, 255, 255, 255, 255), @@ -1688,11 +1903,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (255, 217, 185)} _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (27.429, 27.451)} + _, data = entity5.last_call("turn_on") # The midpoint the white channels is warm, compensated by decreasing green + blue assert data == {"brightness": 128, "rgbw_color": (96, 44, 0, 255)} - _, data = entity5.last_call("turn_on") - assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 3451, "color_temp": 289} @@ -1705,6 +1922,7 @@ async def test_light_service_call_color_conversion_named_tuple( MockLight("Test_rgb", STATE_ON), MockLight("Test_xy", STATE_ON), MockLight("Test_all", STATE_ON), + MockLight("Test_legacy", STATE_ON), MockLight("Test_rgbw", STATE_ON), MockLight("Test_rgbww", STATE_ON), ] @@ -1727,10 +1945,16 @@ async def test_light_service_call_color_conversion_named_tuple( } entity4 = entities[4] - entity4.supported_color_modes = {light.ColorMode.RGBW} + entity4.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity5 = entities[5] - entity5.supported_color_modes = {light.ColorMode.RGBWW} + entity5.supported_color_modes = {light.ColorMode.RGBW} + + entity6 = entities[6] + entity6.supported_color_modes = {light.ColorMode.RGBWW} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1746,6 +1970,7 @@ async def test_light_service_call_color_conversion_named_tuple( entity3.entity_id, entity4.entity_id, entity5.entity_id, + entity6.entity_id, ], "brightness_pct": 25, "rgb_color": color_util.RGBColor(128, 0, 0), @@ -1761,8 +1986,10 @@ async def test_light_service_call_color_conversion_named_tuple( _, data = entity3.last_call("turn_on") assert data == {"brightness": 64, "rgb_color": (128, 0, 0)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 64, "rgbw_color": (128, 0, 0, 0)} + assert data == {"brightness": 64, "hs_color": (0.0, 100.0)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 64, "rgbw_color": (128, 0, 0, 0)} + _, data = entity6.last_call("turn_on") assert data == {"brightness": 64, "rgbww_color": (128, 0, 0, 0, 0)} @@ -2131,6 +2358,13 @@ async def test_light_state_color_conversion(hass: HomeAssistant) -> None: entity2.rgb_color = "Invalid" # Should be ignored entity2.xy_color = (0.1, 0.8) + entity3 = entities[3] + entity3.hs_color = (240, 100) + entity3.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -2152,6 +2386,12 @@ async def test_light_state_color_conversion(hass: HomeAssistant) -> None: assert state.attributes["rgb_color"] == (0, 255, 22) assert state.attributes["xy_color"] == (0.1, 0.8) + state = hass.states.get(entity3.entity_id) + assert state.attributes["color_mode"] == light.ColorMode.HS + assert state.attributes["hs_color"] == (240, 100) + assert state.attributes["rgb_color"] == (0, 0, 255) + assert state.attributes["xy_color"] == (0.136, 0.04) + async def test_services_filter_parameters( hass: HomeAssistant, @@ -2386,6 +2626,27 @@ def test_filter_supported_color_modes() -> None: assert light.filter_supported_color_modes(supported) == {light.ColorMode.BRIGHTNESS} +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockLightEntityEntity(light.LightEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockLightEntityEntity() + assert entity.supported_features_compat is light.LightEntityFeature(1) + assert "MockLightEntityEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "LightEntityFeature" in caplog.text + assert "and color modes" in caplog.text + caplog.clear() + assert entity.supported_features_compat is light.LightEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text + + @pytest.mark.parametrize( ("color_mode", "supported_color_modes", "warning_expected"), [ diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 7c64f846df165..a45fa5b666874 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -129,7 +129,7 @@ def test_support_properties(property_suffix: str) -> None: entity3 = MediaPlayerEntity() entity3._attr_supported_features = feature entity4 = MediaPlayerEntity() - entity4._attr_supported_features = all_features & ~feature + entity4._attr_supported_features = all_features - feature assert getattr(entity1, f"support_{property_suffix}") is False assert getattr(entity2, f"support_{property_suffix}") is True @@ -447,3 +447,23 @@ async def test_get_async_get_browse_image_quoting( url = player.get_browse_image_url("album", media_content_id) await client.get(url) mock_browse_image.assert_called_with("album", media_content_id, None) + + +def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: + """Test deprecated supported features ints.""" + + class MockMediaPlayerEntity(MediaPlayerEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockMediaPlayerEntity() + assert entity.supported_features_compat is MediaPlayerEntityFeature(1) + assert "MockMediaPlayerEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "MediaPlayerEntityFeature.PAUSE" in caplog.text + caplog.clear() + assert entity.supported_features_compat is MediaPlayerEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 2d151b4b81e6a..14229e836628e 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -711,4 +711,4 @@ async def test_no_other_imports_allowed( source = "import sys" hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done(wait_background_tasks=True) - assert "Error executing script: Not allowed to import sys" in caplog.text + assert "ImportError: Not allowed to import sys" in caplog.text diff --git a/tests/components/reolink/test_views.py b/tests/components/reolink/test_views.py index 1eb184950bcae..c994cc59c5d69 100644 --- a/tests/components/reolink/test_views.py +++ b/tests/components/reolink/test_views.py @@ -22,7 +22,7 @@ TEST_DAY2 = 15 TEST_HOUR = 13 TEST_MINUTE = 12 -TEST_FILE_NAME_MP4 = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00.mp4" +TEST_FILE_NAME_MP4 = f"Mp4Record/{TEST_YEAR}-{TEST_MONTH}-{TEST_DAY}/RecS04_{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00_123456_AB123C.mp4" TEST_STREAM = "sub" TEST_CHANNEL = "0" TEST_VOD_TYPE = VodRequestType.PLAYBACK.value diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 357c644e2fe95..44084574e01ce 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -161,6 +161,7 @@ def mock_roborock_entry(hass: HomeAssistant) -> MockConfigEntry: CONF_USER_DATA: USER_DATA.as_dict(), CONF_BASE_URL: BASE_URL, }, + unique_id=USER_EMAIL, ) mock_entry.add_to_hass(hass) return mock_entry diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index 39d8117847c28..13bc23e6e2bf5 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -244,3 +244,28 @@ async def test_reauth_flow( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_roborock_entry.data["user_data"]["rriot"]["s"] == "new_password_hash" + + +async def test_account_already_configured( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, +) -> None: + """Handle the config flow and make sure it succeeds.""" + with patch( + "homeassistant.components.roborock.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USER_EMAIL} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured_account" diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index cace9a8ed67d7..4cd2a37effcf3 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -4,7 +4,12 @@ from unittest.mock import patch import pytest -from roborock import RoborockException, RoborockInvalidCredentials +from roborock import ( + RoborockException, + RoborockInvalidCredentials, + RoborockInvalidUserAgreement, + RoborockNoUserAgreement, +) from homeassistant.components.roborock.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -194,3 +199,35 @@ async def test_not_supported_a01_device( await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() assert "The device you added is not yet supported" in caplog.text + + +async def test_invalid_user_agreement( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, +) -> None: + """Test that we fail setting up if the user agreement is out of date.""" + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + side_effect=RoborockInvalidUserAgreement(), + ): + await hass.config_entries.async_setup(mock_roborock_entry.entry_id) + assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY + assert ( + mock_roborock_entry.error_reason_translation_key == "invalid_user_agreement" + ) + + +async def test_no_user_agreement( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, +) -> None: + """Test that we fail setting up if the user has no agreement.""" + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + side_effect=RoborockNoUserAgreement(), + ): + await hass.config_entries.async_setup(mock_roborock_entry.entry_id) + assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY + assert mock_roborock_entry.error_reason_translation_key == "no_user_agreement" diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index 2dc0cabeaa61d..7b00711442008 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -137,6 +137,7 @@ async def mock_async_browse( "title": "Fake Item 1", "id": FAKE_VALID_ITEM_ID, "hasitems": False, + "isaudio": True, "item_type": child_types[media_type], "artwork_track_id": "b35bb9e9", "url": "file:///var/lib/squeezeboxserver/music/track_1.mp3", @@ -145,6 +146,7 @@ async def mock_async_browse( "title": "Fake Item 2", "id": FAKE_VALID_ITEM_ID + "_2", "hasitems": media_type == "favorites", + "isaudio": True, "item_type": child_types[media_type], "image_url": "http://lms.internal:9000/html/images/favorites.png", "url": "file:///var/lib/squeezeboxserver/music/track_2.mp3", @@ -153,6 +155,7 @@ async def mock_async_browse( "title": "Fake Item 3", "id": FAKE_VALID_ITEM_ID + "_3", "hasitems": media_type == "favorites", + "isaudio": True, "album_id": FAKE_VALID_ITEM_ID if media_type == "favorites" else None, "url": "file:///var/lib/squeezeboxserver/music/track_3.mp3", }, diff --git a/tests/components/unifiprotect/fixtures/sample_bootstrap.json b/tests/components/unifiprotect/fixtures/sample_bootstrap.json index 240a9938b6496..4c8d86a787d28 100644 --- a/tests/components/unifiprotect/fixtures/sample_bootstrap.json +++ b/tests/components/unifiprotect/fixtures/sample_bootstrap.json @@ -564,6 +564,24 @@ "legacyUFVs": [], "lastUpdateId": "ebf25bac-d5a1-4f1d-a0ee-74c15981eb70", "displays": [], + "ringtones": [ + { + "id": "66a14fa502d44203e40003eb", + "name": "Default", + "size": 208, + "isDefault": true, + "nvrMac": "A1E00C826924", + "modelKey": "ringtone" + }, + { + "id": "66a14fa502da4203e40003ec", + "name": "Traditional", + "size": 180, + "isDefault": false, + "nvrMac": "A1E00C826924", + "modelKey": "ringtone" + } + ], "bridges": [ { "mac": "A28D0DB15AE1", diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index db6cd242f3fa9..8babd9fa2659e 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -272,6 +272,42 @@ def send_command( assert "test" in strings +async def test_supported_features_compat(hass: HomeAssistant) -> None: + """Test StateVacuumEntity using deprecated feature constants features.""" + + features = ( + VacuumEntityFeature.BATTERY + | VacuumEntityFeature.FAN_SPEED + | VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.PAUSE + ) + + class _LegacyConstantsStateVacuum(StateVacuumEntity): + _attr_supported_features = int(features) + _attr_fan_speed_list = ["silent", "normal", "pet hair"] + + entity = _LegacyConstantsStateVacuum() + assert isinstance(entity.supported_features, int) + assert entity.supported_features == int(features) + assert entity.supported_features_compat is ( + VacuumEntityFeature.BATTERY + | VacuumEntityFeature.FAN_SPEED + | VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.PAUSE + ) + assert entity.state_attributes == { + "battery_level": None, + "battery_icon": "mdi:battery-unknown", + "fan_speed": None, + } + assert entity.capability_attributes == { + "fan_speed_list": ["silent", "normal", "pet hair"] + } + assert entity._deprecated_supported_features_reported + + async def test_vacuum_not_log_deprecated_state_warning( hass: HomeAssistant, mock_vacuum_entity: MockVacuum, diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index dc579ab6e8d76..2bf441f70fd22 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -4,6 +4,7 @@ from collections.abc import Iterable import dataclasses from datetime import timedelta +from enum import IntFlag import logging import threading from typing import Any @@ -2485,6 +2486,31 @@ def _attr_attribution(self): return "🤡" +async def test_entity_report_deprecated_supported_features_values( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test reporting deprecated supported feature values only happens once.""" + ent = entity.Entity() + + class MockEntityFeatures(IntFlag): + VALUE1 = 1 + VALUE2 = 2 + + ent._report_deprecated_supported_features_values(MockEntityFeatures(2)) + assert ( + "is using deprecated supported features values which will be removed" + in caplog.text + ) + assert "MockEntityFeatures.VALUE2" in caplog.text + + caplog.clear() + ent._report_deprecated_supported_features_values(MockEntityFeatures(2)) + assert ( + "is using deprecated supported features values which will be removed" + not in caplog.text + ) + + async def test_remove_entity_registry( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: