This commit is contained in:
Franck Nijhof 2025-01-07 08:43:32 +01:00 committed by GitHub
commit d59a91a905
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 922 additions and 238 deletions

View File

@ -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"
}
}

View File

@ -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)"
}
}

View File

@ -435,6 +435,7 @@ class BackupManager:
# 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):

View File

@ -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"
]
}

View File

@ -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"]
}

View File

@ -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%]"

View File

@ -516,6 +516,19 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""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 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""
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()

View File

@ -181,6 +181,11 @@ class CloudBackupAgent(BackupAgent):
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

View File

@ -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 @@ PLATFORMS: list[Platform] = [Platform.TODO]
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],
),
)

View File

@ -10,7 +10,6 @@ from cookidoo_api import (
Cookidoo,
CookidooAuthException,
CookidooConfig,
CookidooLocalizationConfig,
CookidooRequestException,
get_country_options,
get_localization_options,
@ -219,18 +218,19 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
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:

View File

@ -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"]
}

View File

@ -300,6 +300,10 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 = (

View File

@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["openwebif"],
"requirements": ["openwebifpy==4.3.0"]
"requirements": ["openwebifpy==4.3.1"]
}

View File

@ -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"]
}

View File

@ -7,7 +7,6 @@ from typing import TYPE_CHECKING
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

View File

@ -6,8 +6,6 @@ from dataclasses import dataclass, field
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.

View File

@ -423,9 +423,7 @@ class ESPHomeManager:
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 (

View File

@ -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."]
}

View File

@ -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 @@ class HassFlickAuth(AbstractFlickAuth):
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)

View File

@ -214,6 +214,18 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
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."""

View File

@ -68,23 +68,14 @@ class FritzBoxBaseEntity:
"""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,
)

View File

@ -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"]
}

View File

@ -114,6 +114,7 @@ class HiveDeviceLight(HiveEntity, LightEntity):
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

View File

@ -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"]
}

View File

@ -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,

View File

@ -220,7 +220,7 @@ async def async_setup_entry(
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:

View File

@ -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."]
}

View File

@ -188,8 +188,8 @@ PINECIL_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = (
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,

View File

@ -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": {

View File

@ -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"

View File

@ -50,7 +50,7 @@ NUMBERS = [
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)),
),

View File

@ -53,6 +53,6 @@
"requirements": [
"aiolifx==1.1.2",
"aiolifx-effects==0.3.2",
"aiolifx-themes==0.5.5"
"aiolifx-themes==0.6.0"
]
}

View File

@ -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 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""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:

View File

@ -57,6 +57,9 @@
},
"valve_position": {
"default": "mdi:valve"
},
"battery_replacement_description": {
"default": "mdi:battery-sync-outline"
}
}
}

View File

@ -773,6 +773,19 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""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 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@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 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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"

View File

@ -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-*" }]
}

View File

@ -37,6 +37,7 @@ DESCRIPTIONS: tuple[PeblarUpdateEntityDescription, ...] = (
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(

View File

@ -7,6 +7,7 @@ from powerfox import (
Powerfox,
PowerfoxAuthenticationError,
PowerfoxConnectionError,
PowerfoxNoDataError,
Poweropti,
)
@ -45,5 +46,5 @@ class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[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

View File

@ -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.",

View File

@ -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:

View File

@ -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 @@ class PlaybackProxyView(HomeAssistantView):
"""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 @@ class PlaybackProxyView(HomeAssistantView):
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))

View File

@ -9,7 +9,13 @@ from datetime import timedelta
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] = {

View File

@ -60,7 +60,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
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)

View File

@ -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": {

View File

@ -73,7 +73,6 @@ class SlideConfigFlow(ConfigFlow, domain=DOMAIN):
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 @@ class SlideConfigFlow(ConfigFlow, domain=DOMAIN):
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 @@ class SlideConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
self._host = discovery_info.host
self._host = ip
return await self.async_step_zeroconf_confirm()

View File

@ -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"]
}

View File

@ -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,
)

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["pysuez", "regex"],
"quality_scale": "bronze",
"requirements": ["pysuezV2==1.3.5"]
"requirements": ["pysuezV2==2.0.1"]
}

View File

@ -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"]
}

View File

@ -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%]"

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["twentemilieu"],
"quality_scale": "silver",
"requirements": ["twentemilieu==2.2.0"]
"requirements": ["twentemilieu==2.2.1"]
}

View File

@ -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",

View File

@ -312,7 +312,7 @@ class StateVacuumEntity(
@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 @@ class StateVacuumEntity(
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 @@ class StateVacuumEntity(
"""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

View File

@ -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%]",

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.63"]
"requirements": ["holidays==0.64"]
}

View File

@ -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",

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
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)

View File

@ -7,7 +7,7 @@ import asyncio
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 @@ class Entity(
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."""

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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

View File

@ -4,7 +4,6 @@ from __future__ import annotations
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,

View File

@ -10,11 +10,16 @@ from homeassistant.components.home_connect.const import (
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"),
[

View File

@ -620,10 +620,10 @@
}),
'area_id': None,
'capabilities': dict({
'max': 12,
'max': 120,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
'step': 5,
}),
'config_entry_id': <ANY>,
'device_class': None,
@ -656,10 +656,10 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pinecil Power limit',
'max': 12,
'max': 120,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
'step': 5,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,

View File

@ -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",

View File

@ -2,6 +2,7 @@
# name: test_diagnostics
dict({
'audio': dict({
'available': True,
'volume': 100,
'volume_limit': dict({
'range_max': 100,

View File

@ -26,7 +26,6 @@ from homeassistant.components.light import (
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

View File

@ -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 @@ invalid_no_brightness_no_color_no_transition,,,
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"),
[

View File

@ -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

View File

@ -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

View File

@ -22,7 +22,7 @@ TEST_DAY = 14
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

View File

@ -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

View File

@ -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"

View File

@ -4,7 +4,12 @@ from copy import deepcopy
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"

View File

@ -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",
},

View File

@ -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",

View File

@ -272,6 +272,42 @@ async def test_send_command(hass: HomeAssistant, config_flow_fixture: None) -> N
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,

View File

@ -4,6 +4,7 @@ import asyncio
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 @@ async def test_cached_entity_property_override(hass: HomeAssistant) -> None:
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: