This commit is contained in:
Franck Nijhof
2025-08-11 21:06:26 +02:00
committed by GitHub
64 changed files with 2011 additions and 561 deletions

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airos",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["airos==0.2.4"]
"requirements": ["airos==0.2.7"]
}

View File

@@ -25,7 +25,7 @@
"services": {
"press": {
"name": "Press",
"description": "Press the button entity."
"description": "Presses a button entity."
}
}
}

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.111.1"],
"requirements": ["hass-nabucasa==0.111.2"],
"single_config_entry": true
}

View File

@@ -1,5 +1,6 @@
"""Data update coordinator for the Enigma2 integration."""
import asyncio
import logging
from openwebif.api import OpenWebIfDevice, OpenWebIfStatus
@@ -30,6 +31,8 @@ from .const import CONF_SOURCE_BOUQUET, DOMAIN
LOGGER = logging.getLogger(__package__)
SETUP_TIMEOUT = 10
type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator]
@@ -79,7 +82,7 @@ class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]):
async def _async_setup(self) -> None:
"""Provide needed data to the device info."""
about = await self.device.get_about()
about = await asyncio.wait_for(self.device.get_about(), timeout=SETUP_TIMEOUT)
self.device.mac_address = about["info"]["ifaces"][0]["mac"]
self.device_info["model"] = about["info"]["model"]
self.device_info["manufacturer"] = about["info"]["brand"]

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250806.0"]
"requirements": ["home-assistant-frontend==20250811.0"]
}

View File

@@ -12,6 +12,7 @@ from aioautomower.exceptions import (
ApiError,
AuthError,
HusqvarnaTimeoutError,
HusqvarnaWSClientError,
HusqvarnaWSServerHandshakeError,
)
from aioautomower.model import MowerDictionary
@@ -142,7 +143,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
# Reset reconnect time after successful connection
self.reconnect_time = DEFAULT_RECONNECT_TIME
await automower_client.start_listening()
except HusqvarnaWSServerHandshakeError as err:
except (HusqvarnaWSServerHandshakeError, HusqvarnaWSClientError) as err:
_LOGGER.debug(
"Failed to connect to websocket. Trying to reconnect: %s",
err,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["imgw_pib==1.5.2"]
"requirements": ["imgw_pib==1.5.3"]
}

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.8.0",
"xknxproject==3.8.2",
"knx-frontend==2025.8.6.52906"
"knx-frontend==2025.8.9.63154"
],
"single_config_entry": true
}

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "Do you want to configure the Launch Library?"
"description": "Do you want to configure Launch Library?"
}
}
},

View File

@@ -49,6 +49,7 @@ from .const import (
CONF_RECOMMENDED,
CONF_TEMPERATURE,
CONF_TOP_P,
CONF_VERBOSITY,
CONF_WEB_SEARCH,
CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_CONTEXT_SIZE,
@@ -67,6 +68,7 @@ from .const import (
RECOMMENDED_REASONING_EFFORT,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_TOP_P,
RECOMMENDED_VERBOSITY,
RECOMMENDED_WEB_SEARCH,
RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE,
RECOMMENDED_WEB_SEARCH_USER_LOCATION,
@@ -323,7 +325,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
model = options[CONF_CHAT_MODEL]
if model.startswith("o"):
if model.startswith(("o", "gpt-5")):
step_schema.update(
{
vol.Optional(
@@ -331,7 +333,9 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
default=RECOMMENDED_REASONING_EFFORT,
): SelectSelector(
SelectSelectorConfig(
options=["low", "medium", "high"],
options=["low", "medium", "high"]
if model.startswith("o")
else ["minimal", "low", "medium", "high"],
translation_key=CONF_REASONING_EFFORT,
mode=SelectSelectorMode.DROPDOWN,
)
@@ -341,6 +345,24 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
elif CONF_REASONING_EFFORT in options:
options.pop(CONF_REASONING_EFFORT)
if model.startswith("gpt-5"):
step_schema.update(
{
vol.Optional(
CONF_VERBOSITY,
default=RECOMMENDED_VERBOSITY,
): SelectSelector(
SelectSelectorConfig(
options=["low", "medium", "high"],
translation_key=CONF_VERBOSITY,
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)
elif CONF_VERBOSITY in options:
options.pop(CONF_VERBOSITY)
if self._subentry_type == "conversation" and not model.startswith(
tuple(UNSUPPORTED_WEB_SEARCH_MODELS)
):

View File

@@ -21,6 +21,7 @@ CONF_REASONING_EFFORT = "reasoning_effort"
CONF_RECOMMENDED = "recommended"
CONF_TEMPERATURE = "temperature"
CONF_TOP_P = "top_p"
CONF_VERBOSITY = "verbosity"
CONF_WEB_SEARCH = "web_search"
CONF_WEB_SEARCH_USER_LOCATION = "user_location"
CONF_WEB_SEARCH_CONTEXT_SIZE = "search_context_size"
@@ -34,6 +35,7 @@ RECOMMENDED_MAX_TOKENS = 3000
RECOMMENDED_REASONING_EFFORT = "low"
RECOMMENDED_TEMPERATURE = 1.0
RECOMMENDED_TOP_P = 1.0
RECOMMENDED_VERBOSITY = "medium"
RECOMMENDED_WEB_SEARCH = False
RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE = "medium"
RECOMMENDED_WEB_SEARCH_USER_LOCATION = False

View File

@@ -61,6 +61,7 @@ from .const import (
CONF_REASONING_EFFORT,
CONF_TEMPERATURE,
CONF_TOP_P,
CONF_VERBOSITY,
CONF_WEB_SEARCH,
CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_CONTEXT_SIZE,
@@ -75,6 +76,7 @@ from .const import (
RECOMMENDED_REASONING_EFFORT,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_TOP_P,
RECOMMENDED_VERBOSITY,
RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE,
)
@@ -346,14 +348,18 @@ class OpenAIBaseLLMEntity(Entity):
if tools:
model_args["tools"] = tools
if model_args["model"].startswith("o"):
if model_args["model"].startswith(("o", "gpt-5")):
model_args["reasoning"] = {
"effort": options.get(
CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT
)
}
else:
model_args["store"] = False
model_args["include"] = ["reasoning.encrypted_content"]
if model_args["model"].startswith("gpt-5"):
model_args["text"] = {
"verbosity": options.get(CONF_VERBOSITY, RECOMMENDED_VERBOSITY)
}
messages = [
m

View File

@@ -121,6 +121,7 @@
"selector": {
"reasoning_effort": {
"options": {
"minimal": "Minimal",
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]",
"high": "[%key:common::state::high%]"
@@ -132,6 +133,13 @@
"medium": "[%key:common::state::medium%]",
"high": "[%key:common::state::high%]"
}
},
"verbosity": {
"options": {
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]",
"high": "[%key:common::state::high%]"
}
}
},
"services": {

View File

@@ -30,9 +30,9 @@ async def validate_input(hass: HomeAssistant, data):
return {
"title": is_valid["title"],
"relay_count": is_valid["relay_count"],
"input_count": is_valid["input_count"],
"is_old": is_valid["is_old"],
"relay_count": is_valid["relays"],
"input_count": is_valid["inputs"],
"is_old": is_valid["temps"],
}

View File

@@ -186,6 +186,9 @@ MODELS_TV_ONLY = (
"ULTRA",
)
MODELS_LINEIN_AND_TV = ("AMP",)
MODEL_SONOS_ARC_ULTRA = "SONOS ARC ULTRA"
ATTR_SPEECH_ENHANCEMENT_ENABLED = "speech_enhance_enabled"
AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1)
AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5

View File

@@ -35,6 +35,7 @@ from homeassistant.util import dt as dt_util
from .alarms import SonosAlarms
from .const import (
ATTR_SPEECH_ENHANCEMENT_ENABLED,
AVAILABILITY_TIMEOUT,
BATTERY_SCAN_INTERVAL,
DOMAIN,
@@ -157,6 +158,7 @@ class SonosSpeaker:
# Home theater
self.audio_delay: int | None = None
self.dialog_level: bool | None = None
self.speech_enhance_enabled: bool | None = None
self.night_mode: bool | None = None
self.sub_enabled: bool | None = None
self.sub_crossover: int | None = None
@@ -548,6 +550,11 @@ class SonosSpeaker:
@callback
def async_update_volume(self, event: SonosEvent) -> None:
"""Update information about currently volume settings."""
_LOGGER.debug(
"Updating volume for %s with event variables: %s",
self.zone_name,
event.variables,
)
self.event_stats.process(event)
variables = event.variables
@@ -565,6 +572,7 @@ class SonosSpeaker:
for bool_var in (
"dialog_level",
ATTR_SPEECH_ENHANCEMENT_ENABLED,
"night_mode",
"sub_enabled",
"surround_enabled",

View File

@@ -19,7 +19,9 @@ from homeassistant.helpers.event import async_track_time_change
from .alarms import SonosAlarms
from .const import (
ATTR_SPEECH_ENHANCEMENT_ENABLED,
DOMAIN,
MODEL_SONOS_ARC_ULTRA,
SONOS_ALARMS_UPDATED,
SONOS_CREATE_ALARM,
SONOS_CREATE_SWITCHES,
@@ -59,6 +61,7 @@ ALL_FEATURES = (
ATTR_SURROUND_ENABLED,
ATTR_STATUS_LIGHT,
)
ALL_SUBST_FEATURES = (ATTR_SPEECH_ENHANCEMENT_ENABLED,)
COORDINATOR_FEATURES = ATTR_CROSSFADE
@@ -69,6 +72,14 @@ POLL_REQUIRED = (
WEEKEND_DAYS = (0, 6)
# Mapping of model names to feature attributes that need to be substituted.
# This is used to handle differences in attributes across Sonos models.
MODEL_FEATURE_SUBSTITUTIONS: dict[str, dict[str, str]] = {
MODEL_SONOS_ARC_ULTRA: {
ATTR_SPEECH_ENHANCEMENT: ATTR_SPEECH_ENHANCEMENT_ENABLED,
},
}
async def async_setup_entry(
hass: HomeAssistant,
@@ -92,6 +103,13 @@ async def async_setup_entry(
def available_soco_attributes(speaker: SonosSpeaker) -> list[str]:
features = []
for feature_type in ALL_SUBST_FEATURES:
try:
if (state := getattr(speaker.soco, feature_type, None)) is not None:
setattr(speaker, feature_type, state)
except SoCoSlaveException:
pass
for feature_type in ALL_FEATURES:
try:
if (state := getattr(speaker.soco, feature_type, None)) is not None:
@@ -107,12 +125,23 @@ async def async_setup_entry(
available_soco_attributes, speaker
)
for feature_type in available_features:
attribute_key = MODEL_FEATURE_SUBSTITUTIONS.get(
speaker.model_name.upper(), {}
).get(feature_type, feature_type)
_LOGGER.debug(
"Creating %s switch on %s",
"Creating %s switch on %s attribute %s",
feature_type,
speaker.zone_name,
attribute_key,
)
entities.append(
SonosSwitchEntity(
feature_type=feature_type,
attribute_key=attribute_key,
speaker=speaker,
config_entry=config_entry,
)
)
entities.append(SonosSwitchEntity(feature_type, speaker, config_entry))
async_add_entities(entities)
config_entry.async_on_unload(
@@ -127,11 +156,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
"""Representation of a Sonos feature switch."""
def __init__(
self, feature_type: str, speaker: SonosSpeaker, config_entry: SonosConfigEntry
self,
feature_type: str,
attribute_key: str,
speaker: SonosSpeaker,
config_entry: SonosConfigEntry,
) -> None:
"""Initialize the switch."""
super().__init__(speaker, config_entry)
self.feature_type = feature_type
self.attribute_key = attribute_key
self.needs_coordinator = feature_type in COORDINATOR_FEATURES
self._attr_entity_category = EntityCategory.CONFIG
self._attr_translation_key = feature_type
@@ -149,15 +182,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
@soco_error()
def poll_state(self) -> None:
"""Poll the current state of the switch."""
state = getattr(self.soco, self.feature_type)
setattr(self.speaker, self.feature_type, state)
state = getattr(self.soco, self.attribute_key)
setattr(self.speaker, self.attribute_key, state)
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
if self.needs_coordinator and not self.speaker.is_coordinator:
return cast(bool, getattr(self.speaker.coordinator, self.feature_type))
return cast(bool, getattr(self.speaker, self.feature_type))
return cast(bool, getattr(self.speaker.coordinator, self.attribute_key))
return cast(bool, getattr(self.speaker, self.attribute_key))
def turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
@@ -175,7 +208,7 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
else:
soco = self.soco
try:
setattr(soco, self.feature_type, enable)
setattr(soco, self.attribute_key, enable)
except SoCoUPnPException as exc:
_LOGGER.warning("Could not toggle %s: %s", self.entity_id, exc)

View File

@@ -157,26 +157,28 @@ class BrowseData:
cmd = ["apps", 0, browse_limit]
result = await player.async_query(*cmd)
for app in result["appss_loop"]:
app_cmd = "app-" + app["cmd"]
if app_cmd not in self.known_apps_radios:
self.add_new_command(app_cmd, "item_id")
_LOGGER.debug(
"Adding new command %s to browse data for player %s",
app_cmd,
player.player_id,
)
if result["appss_loop"]:
for app in result["appss_loop"]:
app_cmd = "app-" + app["cmd"]
if app_cmd not in self.known_apps_radios:
self.add_new_command(app_cmd, "item_id")
_LOGGER.debug(
"Adding new command %s to browse data for player %s",
app_cmd,
player.player_id,
)
cmd = ["radios", 0, browse_limit]
result = await player.async_query(*cmd)
for app in result["radioss_loop"]:
app_cmd = "app-" + app["cmd"]
if app_cmd not in self.known_apps_radios:
self.add_new_command(app_cmd, "item_id")
_LOGGER.debug(
"Adding new command %s to browse data for player %s",
app_cmd,
player.player_id,
)
if result["radioss_loop"]:
for app in result["radioss_loop"]:
app_cmd = "app-" + app["cmd"]
if app_cmd not in self.known_apps_radios:
self.add_new_command(app_cmd, "item_id")
_LOGGER.debug(
"Adding new command %s to browse data for player %s",
app_cmd,
player.player_id,
)
def _build_response_apps_radios_category(

View File

@@ -325,7 +325,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
@property
def volume_level(self) -> float | None:
"""Volume level of the media player (0..1)."""
if self._player.volume:
if self._player.volume is not None:
return int(float(self._player.volume)) / 100.0
return None

View File

@@ -299,7 +299,10 @@ async def async_setup_entry(
)
await home.rt_subscribe(
TibberRtDataCoordinator(
entity_creator.add_sensors, home, hass
hass,
entry,
entity_creator.add_sensors,
home,
).async_set_updated_data
)
@@ -613,15 +616,17 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
add_sensor_callback: Callable[[TibberRtDataCoordinator, Any], None],
tibber_home: tibber.TibberHome,
hass: HomeAssistant,
) -> None:
"""Initialize the data handler."""
self._add_sensor_callback = add_sensor_callback
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=tibber_home.info["viewer"]["home"]["address"].get(
"address1", "Tibber"
),

View File

@@ -99,8 +99,22 @@ class EnumTypeData:
return cls(dpcode, **parsed)
class ComplexTypeData:
"""Complex Type Data (for JSON/RAW parsing)."""
@classmethod
def from_json(cls, data: str) -> Self:
"""Load JSON string and return a ComplexTypeData object."""
raise NotImplementedError("from_json is not implemented for this type")
@classmethod
def from_raw(cls, data: str) -> Self | None:
"""Decode base64 string and return a ComplexTypeData object."""
raise NotImplementedError("from_raw is not implemented for this type")
@dataclass
class ElectricityTypeData:
class ElectricityTypeData(ComplexTypeData):
"""Electricity Type Data."""
electriccurrent: str | None = None
@@ -113,9 +127,11 @@ class ElectricityTypeData:
return cls(**json.loads(data.lower()))
@classmethod
def from_raw(cls, data: str) -> Self:
def from_raw(cls, data: str) -> Self | None:
"""Decode base64 string and return a ElectricityTypeData object."""
raw = base64.b64decode(data)
if len(raw) == 0:
return None
voltage = struct.unpack(">H", raw[0:2])[0] / 10.0
electriccurrent = struct.unpack(">L", b"\x00" + raw[2:5])[0] / 1000.0
power = struct.unpack(">L", b"\x00" + raw[5:8])[0] / 1000.0

View File

@@ -40,13 +40,14 @@ from .const import (
UnitOfMeasurement,
)
from .entity import TuyaEntity
from .models import ElectricityTypeData, EnumTypeData, IntegerTypeData
from .models import ComplexTypeData, ElectricityTypeData, EnumTypeData, IntegerTypeData
@dataclass(frozen=True)
class TuyaSensorEntityDescription(SensorEntityDescription):
"""Describes Tuya sensor entity."""
complex_type: type[ComplexTypeData] | None = None
subkey: str | None = None
@@ -368,6 +369,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
complex_type=ElectricityTypeData,
subkey="electriccurrent",
),
TuyaSensorEntityDescription(
@@ -376,6 +378,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
complex_type=ElectricityTypeData,
subkey="power",
),
TuyaSensorEntityDescription(
@@ -384,6 +387,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
complex_type=ElectricityTypeData,
subkey="voltage",
),
TuyaSensorEntityDescription(
@@ -392,6 +396,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
complex_type=ElectricityTypeData,
subkey="electriccurrent",
),
TuyaSensorEntityDescription(
@@ -400,6 +405,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
complex_type=ElectricityTypeData,
subkey="power",
),
TuyaSensorEntityDescription(
@@ -408,6 +414,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
complex_type=ElectricityTypeData,
subkey="voltage",
),
TuyaSensorEntityDescription(
@@ -416,6 +423,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
complex_type=ElectricityTypeData,
subkey="electriccurrent",
),
TuyaSensorEntityDescription(
@@ -424,6 +432,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
complex_type=ElectricityTypeData,
subkey="power",
),
TuyaSensorEntityDescription(
@@ -432,6 +441,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
complex_type=ElectricityTypeData,
subkey="voltage",
),
TuyaSensorEntityDescription(
@@ -1254,6 +1264,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="total_power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
complex_type=ElectricityTypeData,
subkey="power",
),
TuyaSensorEntityDescription(
@@ -1262,6 +1273,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
complex_type=ElectricityTypeData,
subkey="electriccurrent",
),
TuyaSensorEntityDescription(
@@ -1270,6 +1282,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
complex_type=ElectricityTypeData,
subkey="power",
),
TuyaSensorEntityDescription(
@@ -1278,6 +1291,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
complex_type=ElectricityTypeData,
subkey="voltage",
),
TuyaSensorEntityDescription(
@@ -1286,6 +1300,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
complex_type=ElectricityTypeData,
subkey="electriccurrent",
),
TuyaSensorEntityDescription(
@@ -1294,6 +1309,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
complex_type=ElectricityTypeData,
subkey="power",
),
TuyaSensorEntityDescription(
@@ -1302,6 +1318,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
complex_type=ElectricityTypeData,
subkey="voltage",
),
TuyaSensorEntityDescription(
@@ -1310,6 +1327,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
complex_type=ElectricityTypeData,
subkey="electriccurrent",
),
TuyaSensorEntityDescription(
@@ -1318,6 +1336,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
complex_type=ElectricityTypeData,
subkey="power",
),
TuyaSensorEntityDescription(
@@ -1326,6 +1345,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
complex_type=ElectricityTypeData,
subkey="voltage",
),
),
@@ -1424,7 +1444,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
_status_range: DeviceStatusRange | None = None
_type: DPType | None = None
_type_data: IntegerTypeData | EnumTypeData | None = None
_type_data: IntegerTypeData | EnumTypeData | ComplexTypeData | None = None
_uom: UnitOfMeasurement | None = None
def __init__(
@@ -1476,6 +1496,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
self.unique_id,
)
self._attr_device_class = None
self._attr_suggested_unit_of_measurement = None
return
uoms = DEVICE_CLASS_UNITS[self.device_class]
@@ -1486,6 +1507,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
# Unknown unit of measurement, device class should not be used.
if uom is None:
self._attr_device_class = None
self._attr_suggested_unit_of_measurement = None
return
# Found unit of measurement, use the standardized Unit
@@ -1523,16 +1545,23 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
# Get subkey value from Json string.
if self._type is DPType.JSON:
if self.entity_description.subkey is None:
if (
self.entity_description.complex_type is None
or self.entity_description.subkey is None
):
return None
values = ElectricityTypeData.from_json(value)
values = self.entity_description.complex_type.from_json(value)
return getattr(values, self.entity_description.subkey)
if self._type is DPType.RAW:
if self.entity_description.subkey is None:
if (
self.entity_description.complex_type is None
or self.entity_description.subkey is None
or (raw_values := self.entity_description.complex_type.from_raw(value))
is None
):
return None
values = ElectricityTypeData.from_raw(value)
return getattr(values, self.entity_description.subkey)
return getattr(raw_values, self.entity_description.subkey)
# Valid string or enum value
return value

View File

@@ -8,7 +8,7 @@ import logging
from aiohttp.client_exceptions import ServerDisconnectedError
from uiprotect.api import DEVICE_UPDATE_INTERVAL
from uiprotect.data import Bootstrap
from uiprotect.exceptions import ClientError, NotAuthorized
from uiprotect.exceptions import BadRequest, ClientError, NotAuthorized
# Import the test_util.anonymize module from the uiprotect package
# in __init__ to ensure it gets imported in the executor since the
@@ -100,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
new_api_key = await protect.create_api_key(
name=f"Home Assistant ({hass.config.location_name})"
)
except NotAuthorized as err:
except (NotAuthorized, BadRequest) as err:
_LOGGER.error("Failed to create API key: %s", err)
else:
protect.set_api_key(new_api_key)

View File

@@ -104,7 +104,12 @@ def async_migrate_entities_unique_ids(
f"{registry_entry.config_entry_id}_"
).removesuffix(f"_{registry_entry.translation_key}")
if monitor := next(
(m for m in metrics.values() if m.monitor_name == name), None
(
m
for m in metrics.values()
if m.monitor_name == name and m.monitor_id is not None
),
None,
):
entity_registry.async_update_entity(
registry_entry.entity_id,

View File

@@ -79,7 +79,10 @@ DEFAULT_NAME = "Vacuum cleaner robot"
_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2026.1")
_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1")
_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",)
_BATTERY_DEPRECATION_IGNORED_PLATFORMS = (
"mqtt",
"template",
)
class VacuumEntityFeature(IntFlag):
@@ -333,7 +336,7 @@ class StateVacuumEntity(
f"is setting the {property} which has been deprecated."
f" Integration {self.platform.platform_name} should implement a sensor"
" instead with a correct device class and link it to the same device",
core_integration_behavior=ReportBehavior.LOG,
core_integration_behavior=ReportBehavior.IGNORE,
custom_integration_behavior=ReportBehavior.LOG,
breaks_in_ha_version="2026.8",
integration_domain=self.platform.platform_name,
@@ -358,7 +361,7 @@ class StateVacuumEntity(
f" Integration {self.platform.platform_name} should remove this as part of migrating"
" the battery level and icon to a sensor",
core_behavior=ReportBehavior.LOG,
core_integration_behavior=ReportBehavior.LOG,
core_integration_behavior=ReportBehavior.IGNORE,
custom_integration_behavior=ReportBehavior.LOG,
breaks_in_ha_version="2026.8",
integration_domain=self.platform.platform_name,

View File

@@ -15,6 +15,7 @@ from volvocarsapi.models import (
VolvoAuthException,
VolvoCarsApiBaseModel,
VolvoCarsValue,
VolvoCarsValueStatusField,
VolvoCarsVehicle,
)
@@ -36,6 +37,16 @@ type VolvoConfigEntry = ConfigEntry[tuple[VolvoBaseCoordinator, ...]]
type CoordinatorData = dict[str, VolvoCarsApiBaseModel | None]
def _is_invalid_api_field(field: VolvoCarsApiBaseModel | None) -> bool:
if not field:
return True
if isinstance(field, VolvoCarsValueStatusField) and field.status == "ERROR":
return True
return False
class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]):
"""Volvo base coordinator."""
@@ -121,7 +132,13 @@ class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]):
translation_key="update_failed",
) from result
data |= cast(CoordinatorData, result)
api_data = cast(CoordinatorData, result)
data |= {
key: field
for key, field in api_data.items()
if not _is_invalid_api_field(field)
}
valid = True
# Raise an error if not a single API call succeeded

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, replace
from dataclasses import dataclass
import logging
from typing import Any, cast
@@ -47,7 +47,6 @@ _LOGGER = logging.getLogger(__name__)
class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription):
"""Describes a Volvo sensor entity."""
source_fields: list[str] | None = None
value_fn: Callable[[VolvoCarsValue], Any] | None = None
@@ -87,7 +86,12 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None:
return None
_CHARGING_POWER_STATUS_OPTIONS = ["providing_power", "no_power_available"]
_CHARGING_POWER_STATUS_OPTIONS = [
"fault",
"power_available_but_not_activated",
"providing_power",
"no_power_available",
]
_DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
# command-accessibility endpoint
@@ -110,6 +114,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageEnergyConsumption",
native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
# statistics endpoint
VolvoSensorDescription(
@@ -117,6 +122,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageEnergyConsumptionAutomatic",
native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
# statistics endpoint
VolvoSensorDescription(
@@ -124,6 +130,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageEnergyConsumptionSinceCharge",
native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
# statistics endpoint
VolvoSensorDescription(
@@ -131,6 +138,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageFuelConsumption",
native_unit_of_measurement="L/100 km",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
# statistics endpoint
VolvoSensorDescription(
@@ -138,6 +146,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageFuelConsumptionAutomatic",
native_unit_of_measurement="L/100 km",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
# statistics endpoint
VolvoSensorDescription(
@@ -235,11 +244,15 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
"none",
],
),
# statistics & energy state endpoint
# statistics endpoint
# We're not using `electricRange` from the energy state endpoint because
# the official app seems to use `distanceToEmptyBattery`.
# In issue #150213, a user described to behavior as follows:
# - For a `distanceToEmptyBattery` of 250km, the `electricRange` was 150mi
# - For a `distanceToEmptyBattery` of 260km, the `electricRange` was 160mi
VolvoSensorDescription(
key="distance_to_empty_battery",
api_field="",
source_fields=["distanceToEmptyBattery", "electricRange"],
api_field="distanceToEmptyBattery",
native_unit_of_measurement=UnitOfLength.KILOMETERS,
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.MEASUREMENT,
@@ -357,12 +370,7 @@ async def async_setup_entry(
if description.key in added_keys:
continue
if description.source_fields:
for field in description.source_fields:
if field in coordinator.data:
description = replace(description, api_field=field)
_add_entity(coordinator, description)
elif description.api_field in coordinator.data:
if description.api_field in coordinator.data:
_add_entity(coordinator, description)
async_add_entities(entities)

View File

@@ -94,7 +94,7 @@
"state": {
"connected": "[%key:common::state::connected%]",
"disconnected": "[%key:common::state::disconnected%]",
"fault": "[%key:common::state::error%]"
"fault": "[%key:common::state::fault%]"
}
},
"charging_current_limit": {
@@ -106,6 +106,8 @@
"charging_power_status": {
"name": "Charging power status",
"state": {
"fault": "[%key:common::state::fault%]",
"power_available_but_not_activated": "Power available",
"providing_power": "Providing power",
"no_power_available": "No power"
}

View File

@@ -8,6 +8,7 @@ from typing import Any
from zha.application.const import ATTR_IEEE
from zha.application.gateway import Gateway
from zigpy.application import ControllerApplication
from zigpy.config import CONF_NWK_EXTENDED_PAN_ID
from zigpy.types import Channels
@@ -63,6 +64,19 @@ def shallow_asdict(obj: Any) -> dict:
return obj
def get_application_state_diagnostics(app: ControllerApplication) -> dict:
"""Dump the application state as a dictionary."""
data = shallow_asdict(app.state)
# EUI64 objects in zigpy are not subclasses of any JSON-serializable key type and
# must be converted to strings.
data["network_info"]["nwk_addresses"] = {
str(k): v for k, v in data["network_info"]["nwk_addresses"].items()
}
return data
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
@@ -79,7 +93,7 @@ async def async_get_config_entry_diagnostics(
{
"config": zha_data.yaml_config,
"config_entry": config_entry.as_dict(),
"application_state": shallow_asdict(app.state),
"application_state": get_application_state_diagnostics(app),
"energy_scan": {
channel: 100 * energy / 255 for channel, energy in energy_scan.items()
},

View File

@@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["zha==0.0.66"],
"requirements": ["zha==0.0.68"],
"usb": [
{
"vid": "10C4",

View File

@@ -43,7 +43,7 @@ from .models import ZwaveJSConfigEntry
PARALLEL_UPDATES = 1
UPDATE_DELAY_STRING = "delay"
UPDATE_DELAY_INTERVAL = 5 # In minutes
UPDATE_DELAY_INTERVAL = 15 # In seconds
ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware"
@@ -130,11 +130,11 @@ async def async_setup_entry(
@callback
def async_add_firmware_update_entity(node: ZwaveNode) -> None:
"""Add firmware update entity."""
# We need to delay the first update of each entity to avoid flooding the network
# so we maintain a counter to schedule first update in UPDATE_DELAY_INTERVAL
# minute increments.
# Delay the first update of each entity to avoid spamming the firmware server.
# Maintain a counter to schedule first update in UPDATE_DELAY_INTERVAL
# second increments.
cnt[UPDATE_DELAY_STRING] += 1
delay = timedelta(minutes=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL))
delay = timedelta(seconds=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL))
driver = client.driver
assert driver is not None # Driver is ready before platforms are loaded.
if node.is_controller_node:
@@ -429,7 +429,8 @@ class ZWaveFirmwareUpdateEntity(UpdateEntity):
):
self._attr_latest_version = self._attr_installed_version
# Spread updates out in 5 minute increments to avoid flooding the network
# Spread updates out in 15 second increments
# to avoid spamming the firmware server
self.async_on_remove(
async_call_later(self.hass, self._delay, self._async_update)
)

View File

@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 8
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, 13, 2)

View File

@@ -35,10 +35,10 @@ fnv-hash-fast==1.5.0
go2rtc-client==0.2.1
ha-ffmpeg==3.2.2
habluetooth==4.0.2
hass-nabucasa==0.111.1
hass-nabucasa==0.111.2
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250806.0
home-assistant-frontend==20250811.0
home-assistant-intents==2025.7.30
httpx==0.28.1
ifaddr==0.2.0
@@ -213,3 +213,6 @@ multidict>=6.4.2
# Stable Alpine current only ships cargo 1.83.0
# No wheels upstream available for armhf & armv7
rpds-py==0.24.0
# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI
num2words==0.5.14

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.8.0"
version = "2025.8.1"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -47,7 +47,7 @@ dependencies = [
"fnv-hash-fast==1.5.0",
# hass-nabucasa is imported by helpers which don't depend on the cloud
# integration
"hass-nabucasa==0.111.1",
"hass-nabucasa==0.111.2",
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.28.1",

2
requirements.txt generated
View File

@@ -22,7 +22,7 @@ certifi>=2021.5.30
ciso8601==2.3.2
cronsim==2.6
fnv-hash-fast==1.5.0
hass-nabucasa==0.111.1
hass-nabucasa==0.111.2
httpx==0.28.1
home-assistant-bluetooth==1.13.1
ifaddr==0.2.0

12
requirements_all.txt generated
View File

@@ -453,7 +453,7 @@ airgradient==0.9.2
airly==1.1.0
# homeassistant.components.airos
airos==0.2.4
airos==0.2.7
# homeassistant.components.airthings_ble
airthings-ble==0.9.2
@@ -1133,7 +1133,7 @@ habiticalib==0.4.1
habluetooth==4.0.2
# homeassistant.components.cloud
hass-nabucasa==0.111.1
hass-nabucasa==0.111.2
# homeassistant.components.splunk
hass-splunk==0.1.1
@@ -1174,7 +1174,7 @@ hole==0.9.0
holidays==0.78
# homeassistant.components.frontend
home-assistant-frontend==20250806.0
home-assistant-frontend==20250811.0
# homeassistant.components.conversation
home-assistant-intents==2025.7.30
@@ -1240,7 +1240,7 @@ ihcsdk==2.8.5
imeon_inverter_api==0.3.14
# homeassistant.components.imgw_pib
imgw_pib==1.5.2
imgw_pib==1.5.3
# homeassistant.components.incomfort
incomfort-client==0.6.9
@@ -1307,7 +1307,7 @@ kiwiki-client==0.1.1
knocki==0.4.2
# homeassistant.components.knx
knx-frontend==2025.8.6.52906
knx-frontend==2025.8.9.63154
# homeassistant.components.konnected
konnected==1.2.0
@@ -3203,7 +3203,7 @@ zeroconf==0.147.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.66
zha==0.0.68
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -435,7 +435,7 @@ airgradient==0.9.2
airly==1.1.0
# homeassistant.components.airos
airos==0.2.4
airos==0.2.7
# homeassistant.components.airthings_ble
airthings-ble==0.9.2
@@ -994,7 +994,7 @@ habiticalib==0.4.1
habluetooth==4.0.2
# homeassistant.components.cloud
hass-nabucasa==0.111.1
hass-nabucasa==0.111.2
# homeassistant.components.assist_satellite
# homeassistant.components.conversation
@@ -1023,7 +1023,7 @@ hole==0.9.0
holidays==0.78
# homeassistant.components.frontend
home-assistant-frontend==20250806.0
home-assistant-frontend==20250811.0
# homeassistant.components.conversation
home-assistant-intents==2025.7.30
@@ -1074,7 +1074,7 @@ igloohome-api==0.1.1
imeon_inverter_api==0.3.14
# homeassistant.components.imgw_pib
imgw_pib==1.5.2
imgw_pib==1.5.3
# homeassistant.components.incomfort
incomfort-client==0.6.9
@@ -1129,7 +1129,7 @@ kegtron-ble==0.4.0
knocki==0.4.2
# homeassistant.components.knx
knx-frontend==2025.8.6.52906
knx-frontend==2025.8.9.63154
# homeassistant.components.konnected
konnected==1.2.0
@@ -2647,7 +2647,7 @@ zeroconf==0.147.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.66
zha==0.0.68
# homeassistant.components.zwave_js
zwave-js-server-python==0.67.1

View File

@@ -239,6 +239,9 @@ multidict>=6.4.2
# Stable Alpine current only ships cargo 1.83.0
# No wheels upstream available for armhf & armv7
rpds-py==0.24.0
# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI
num2words==0.5.14
"""
GENERATED_MESSAGE = (

View File

@@ -15,7 +15,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture
@pytest.fixture
def ap_fixture():
"""Load fixture data for AP mode."""
json_data = load_json_object_fixture("airos_ap-ptp.json", DOMAIN)
json_data = load_json_object_fixture("airos_loco5ac_ap-ptp.json", DOMAIN)
return AirOSData.from_dict(json_data)

View File

@@ -1,132 +1,194 @@
{
"chain_names": [
{ "number": 1, "name": "Chain 0" },
{ "number": 2, "name": "Chain 1" }
{
"name": "Chain 0",
"number": 1
},
{
"name": "Chain 1",
"number": 2
}
],
"host": {
"hostname": "NanoStation 5AC ap name",
"device_id": "03aa0d0b40fed0a47088293584ef5432",
"uptime": 264888,
"power_time": 268683,
"time": "2025-06-23 23:06:42",
"timestamp": 2668313184,
"fwversion": "v8.7.17",
"devmodel": "NanoStation 5AC loco",
"netrole": "bridge",
"loadavg": 0.412598,
"totalram": 63447040,
"freeram": 16564224,
"temperature": 0,
"cpuload": 10.10101,
"height": 3
},
"genuine": "/images/genuine.png",
"services": {
"dhcpc": false,
"dhcpd": false,
"dhcp6d_stateful": false,
"pppoe": false,
"airview": 2
"derived": {
"access_point": true,
"mac": "01:23:45:67:89:AB",
"mac_interface": "br0",
"ptmp": false,
"ptp": true,
"station": false
},
"firewall": {
"iptables": false,
"eb6tables": false,
"ebtables": false,
"ip6tables": false,
"eb6tables": false
"iptables": false
},
"genuine": "/images/genuine.png",
"gps": {
"fix": 0,
"lat": 52.379894,
"lon": 4.901608
},
"host": {
"cpuload": 10.10101,
"device_id": "03aa0d0b40fed0a47088293584ef5432",
"devmodel": "NanoStation 5AC loco",
"freeram": 16564224,
"fwversion": "v8.7.17",
"height": 3,
"hostname": "NanoStation 5AC ap name",
"loadavg": 0.412598,
"netrole": "bridge",
"power_time": 268683,
"temperature": 0,
"time": "2025-06-23 23:06:42",
"timestamp": 2668313184,
"totalram": 63447040,
"uptime": 264888
},
"interfaces": [
{
"enabled": true,
"hwaddr": "01:23:45:67:89:AB",
"ifname": "eth0",
"mtu": 1500,
"status": {
"cable_len": 18,
"duplex": true,
"ip6addr": null,
"ipaddr": "0.0.0.0",
"plugged": true,
"rx_bytes": 3984971949,
"rx_dropped": 0,
"rx_errors": 4,
"rx_packets": 73564835,
"snr": [30, 30, 30, 30],
"speed": 1000,
"tx_bytes": 209900085624,
"tx_dropped": 10,
"tx_errors": 0,
"tx_packets": 185866883
}
},
{
"enabled": true,
"hwaddr": "01:23:45:67:89:AB",
"ifname": "ath0",
"mtu": 1500,
"status": {
"cable_len": null,
"duplex": false,
"ip6addr": null,
"ipaddr": "0.0.0.0",
"plugged": false,
"rx_bytes": 206938324766,
"rx_dropped": 0,
"rx_errors": 0,
"rx_packets": 149767200,
"snr": null,
"speed": 0,
"tx_bytes": 5265602738,
"tx_dropped": 2005,
"tx_errors": 0,
"tx_packets": 52980390
}
},
{
"enabled": true,
"hwaddr": "01:23:45:67:89:AB",
"ifname": "br0",
"mtu": 1500,
"status": {
"cable_len": null,
"duplex": false,
"ip6addr": [
{
"addr": "fe80::eea:14ff:fea4:89cd",
"plen": 64
}
],
"ipaddr": "192.168.1.2",
"plugged": true,
"rx_bytes": 204802727,
"rx_dropped": 0,
"rx_errors": 0,
"rx_packets": 1791592,
"snr": null,
"speed": 0,
"tx_bytes": 236295176,
"tx_dropped": 0,
"tx_errors": 0,
"tx_packets": 298119
}
}
],
"ntpclient": {},
"portfw": false,
"provmode": {},
"services": {
"airview": 2,
"dhcp6d_stateful": false,
"dhcpc": false,
"dhcpd": false,
"pppoe": false
},
"unms": {
"status": 0,
"timestamp": null
},
"wireless": {
"essid": "DemoSSID",
"mode": "ap-ptp",
"ieeemode": "11ACVHT80",
"band": 2,
"compat_11n": 0,
"hide_essid": 0,
"apmac": "01:23:45:67:89:AB",
"antenna_gain": 13,
"frequency": 5500,
"center1_freq": 5530,
"dfs": 1,
"distance": 0,
"security": "WPA2",
"noisef": -89,
"txpower": -3,
"apmac": "01:23:45:67:89:AB",
"aprepeater": false,
"rstatus": 5,
"chanbw": 80,
"rx_chainmask": 3,
"tx_chainmask": 3,
"nol_state": 0,
"nol_timeout": 0,
"band": 2,
"cac_state": 0,
"cac_timeout": 0,
"rx_idx": 8,
"rx_nss": 2,
"tx_idx": 9,
"tx_nss": 2,
"throughput": { "tx": 222, "rx": 9907 },
"service": { "time": 267181, "link": 266003 },
"center1_freq": 5530,
"chanbw": 80,
"compat_11n": 0,
"count": 1,
"dfs": 1,
"distance": 0,
"essid": "DemoSSID",
"frequency": 5500,
"hide_essid": 0,
"ieeemode": "11ACVHT80",
"mode": "ap-ptp",
"noisef": -89,
"nol_state": 0,
"nol_timeout": 0,
"polling": {
"atpc_status": 2,
"cb_capacity": 593970,
"dl_capacity": 647400,
"ul_capacity": 540540,
"use": 48,
"tx_use": 6,
"rx_use": 42,
"atpc_status": 2,
"ff_cap_rep": false,
"fixed_frame": false,
"flex_mode": null,
"gps_sync": false,
"ff_cap_rep": false
"rx_use": 42,
"tx_use": 6,
"ul_capacity": 540540,
"use": 48
},
"rstatus": 5,
"rx_chainmask": 3,
"rx_idx": 8,
"rx_nss": 2,
"security": "WPA2",
"service": {
"link": 266003,
"time": 267181
},
"count": 1,
"sta": [
{
"mac": "01:23:45:67:89:AB",
"lastip": "192.168.1.2",
"signal": -59,
"rssi": 37,
"noisefloor": -89,
"chainrssi": [35, 32, 0],
"tx_idx": 9,
"rx_idx": 8,
"tx_nss": 2,
"rx_nss": 2,
"tx_latency": 0,
"distance": 1,
"tx_packets": 0,
"tx_lretries": 0,
"tx_sretries": 0,
"uptime": 170281,
"dl_signal_expect": -80,
"ul_signal_expect": -55,
"cb_capacity_expect": 416000,
"dl_capacity_expect": 208000,
"ul_capacity_expect": 624000,
"dl_rate_expect": 3,
"ul_rate_expect": 8,
"dl_linkscore": 100,
"ul_linkscore": 86,
"dl_avg_linkscore": 100,
"ul_avg_linkscore": 88,
"tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430],
"stats": {
"rx_bytes": 206938324814,
"rx_packets": 149767200,
"rx_pps": 846,
"tx_bytes": 5265602739,
"tx_packets": 52980390,
"tx_pps": 0
},
"airmax": {
"actual_priority": 0,
"beam": 0,
"desired_priority": 0,
"cb_capacity": 593970,
"dl_capacity": 647400,
"ul_capacity": 540540,
"atpc_status": 2,
"beam": 0,
"cb_capacity": 593970,
"desired_priority": 0,
"dl_capacity": 647400,
"rx": {
"usage": 42,
"cinr": 31,
"evm": [
[
@@ -141,10 +203,10 @@
34, 33, 34, 34, 34, 34, 34, 35, 35, 35, 34, 35, 33, 34, 34, 34,
34, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35
]
]
],
"usage": 42
},
"tx": {
"usage": 6,
"cinr": 31,
"evm": [
[
@@ -159,142 +221,127 @@
38, 37, 37, 37, 38, 37, 38, 37, 37, 37, 37, 37, 36, 37, 37, 37,
37, 37, 37, 38, 37, 37, 38, 37, 36, 37, 37, 37, 37, 37, 37, 37
]
]
}
],
"usage": 6
},
"ul_capacity": 540540
},
"airos_connected": true,
"cb_capacity_expect": 416000,
"chainrssi": [35, 32, 0],
"distance": 1,
"dl_avg_linkscore": 100,
"dl_capacity_expect": 208000,
"dl_linkscore": 100,
"dl_rate_expect": 3,
"dl_signal_expect": -80,
"last_disc": 1,
"lastip": "192.168.1.2",
"mac": "01:23:45:67:89:AB",
"noisefloor": -89,
"remote": {
"age": 1,
"device_id": "d4f4cdf82961e619328a8f72f8d7653b",
"hostname": "NanoStation 5AC sta name",
"platform": "NanoStation 5AC loco",
"version": "WA.ar934x.v8.7.17.48152.250620.2132",
"time": "2025-06-23 23:13:54",
"cpuload": 43.564301,
"temperature": 0,
"totalram": 63447040,
"freeram": 14290944,
"netrole": "bridge",
"mode": "sta-ptp",
"sys_id": "0xe7fa",
"tx_throughput": 16023,
"rx_throughput": 251,
"uptime": 265320,
"power_time": 268512,
"compat_11n": 0,
"signal": -58,
"rssi": 38,
"noisefloor": -90,
"tx_power": -4,
"distance": 1,
"rx_chainmask": 3,
"airview": 2,
"antenna_gain": 13,
"cable_loss": 0,
"chainrssi": [33, 37, 0],
"compat_11n": 0,
"cpuload": 43.564301,
"device_id": "d4f4cdf82961e619328a8f72f8d7653b",
"distance": 1,
"ethlist": [
{
"cable_len": 14,
"duplex": true,
"enabled": true,
"ifname": "eth0",
"plugged": true,
"snr": [30, 30, 29, 30],
"speed": 1000
}
],
"freeram": 14290944,
"gps": {
"alt": null,
"dim": null,
"dop": null,
"fix": 0,
"lat": 52.379894,
"lon": 4.901608,
"sats": null,
"time_synced": null
},
"height": 2,
"hostname": "NanoStation 5AC sta name",
"ip6addr": ["fe80::eea:14ff:fea4:89ab"],
"ipaddr": ["192.168.1.2"],
"mode": "sta-ptp",
"netrole": "bridge",
"noisefloor": -90,
"oob": false,
"platform": "NanoStation 5AC loco",
"power_time": 268512,
"rssi": 38,
"rx_bytes": 3624206478,
"rx_chainmask": 3,
"rx_throughput": 251,
"service": {
"link": 265996,
"time": 267195
},
"signal": -58,
"sys_id": "0xe7fa",
"temperature": 0,
"time": "2025-06-23 23:13:54",
"totalram": 63447040,
"tx_bytes": 212308148210,
"tx_power": -4,
"tx_ratedata": [
14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154
],
"tx_bytes": 212308148210,
"rx_bytes": 3624206478,
"antenna_gain": 13,
"cable_loss": 0,
"height": 2,
"ethlist": [
{
"ifname": "eth0",
"enabled": true,
"plugged": true,
"duplex": true,
"speed": 1000,
"snr": [30, 30, 29, 30],
"cable_len": 14
}
],
"ipaddr": ["192.168.1.2"],
"ip6addr": ["fe80::eea:14ff:fea4:89ab"],
"gps": { "lat": "52.379894", "lon": "4.901608", "fix": 0 },
"oob": false,
"unms": { "status": 0, "timestamp": null },
"airview": 2,
"service": { "time": 267195, "link": 265996 }
"tx_throughput": 16023,
"unms": {
"status": 0,
"timestamp": null
},
"uptime": 265320,
"version": "WA.ar934x.v8.7.17.48152.250620.2132"
},
"airos_connected": true
"rssi": 37,
"rx_idx": 8,
"rx_nss": 2,
"signal": -59,
"stats": {
"rx_bytes": 206938324814,
"rx_packets": 149767200,
"rx_pps": 846,
"tx_bytes": 5265602739,
"tx_packets": 52980390,
"tx_pps": 0
},
"tx_idx": 9,
"tx_latency": 0,
"tx_lretries": 0,
"tx_nss": 2,
"tx_packets": 0,
"tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430],
"tx_sretries": 0,
"ul_avg_linkscore": 88,
"ul_capacity_expect": 624000,
"ul_linkscore": 86,
"ul_rate_expect": 8,
"ul_signal_expect": -55,
"uptime": 170281
}
],
"sta_disconnected": []
},
"interfaces": [
{
"ifname": "eth0",
"hwaddr": "01:23:45:67:89:AB",
"enabled": true,
"mtu": 1500,
"status": {
"plugged": true,
"tx_bytes": 209900085624,
"rx_bytes": 3984971949,
"tx_packets": 185866883,
"rx_packets": 73564835,
"tx_errors": 0,
"rx_errors": 4,
"tx_dropped": 10,
"rx_dropped": 0,
"ipaddr": "0.0.0.0",
"speed": 1000,
"duplex": true,
"snr": [30, 30, 30, 30],
"cable_len": 18,
"ip6addr": null
}
"sta_disconnected": [],
"throughput": {
"rx": 9907,
"tx": 222
},
{
"ifname": "ath0",
"hwaddr": "01:23:45:67:89:AB",
"enabled": true,
"mtu": 1500,
"status": {
"plugged": false,
"tx_bytes": 5265602738,
"rx_bytes": 206938324766,
"tx_packets": 52980390,
"rx_packets": 149767200,
"tx_errors": 0,
"rx_errors": 0,
"tx_dropped": 2005,
"rx_dropped": 0,
"ipaddr": "0.0.0.0",
"speed": 0,
"duplex": false,
"snr": null,
"cable_len": null,
"ip6addr": null
}
},
{
"ifname": "br0",
"hwaddr": "01:23:45:67:89:AB",
"enabled": true,
"mtu": 1500,
"status": {
"plugged": true,
"tx_bytes": 236295176,
"rx_bytes": 204802727,
"tx_packets": 298119,
"rx_packets": 1791592,
"tx_errors": 0,
"rx_errors": 0,
"tx_dropped": 0,
"rx_dropped": 0,
"ipaddr": "192.168.1.2",
"speed": 0,
"duplex": false,
"snr": null,
"cable_len": null,
"ip6addr": [{ "addr": "fe80::eea:14ff:fea4:89cd", "plen": 64 }]
}
}
],
"provmode": {},
"ntpclient": {},
"unms": { "status": 0, "timestamp": null },
"gps": { "lat": 52.379894, "lon": 4.901608, "fix": 0 },
"derived": { "mac": "01:23:45:67:89:AB", "mac_interface": "br0" }
"tx_chainmask": 3,
"tx_idx": 9,
"tx_nss": 2,
"txpower": -3
}
}

View File

@@ -13,8 +13,12 @@
}),
]),
'derived': dict({
'access_point': True,
'mac': '**REDACTED**',
'mac_interface': 'br0',
'ptmp': False,
'ptp': True,
'station': False,
}),
'firewall': dict({
'eb6tables': False,
@@ -164,6 +168,7 @@
'dl_capacity': 647400,
'ff_cap_rep': False,
'fixed_frame': False,
'flex_mode': None,
'gps_sync': False,
'rx_use': 42,
'tx_use': 6,
@@ -515,9 +520,14 @@
]),
'freeram': 14290944,
'gps': dict({
'alt': None,
'dim': None,
'dop': None,
'fix': 0,
'lat': '**REDACTED**',
'lon': '**REDACTED**',
'sats': None,
'time_synced': None,
}),
'height': 2,
'hostname': '**REDACTED**',

View File

@@ -2,6 +2,7 @@
from copy import deepcopy
import json
import logging
from typing import Any
from unittest.mock import patch
@@ -395,6 +396,15 @@ async def test_status_with_deprecated_battery_feature(
assert issue.issue_domain == "vacuum"
assert issue.translation_key == "deprecated_vacuum_battery_feature"
assert issue.translation_placeholders == {"entity_id": "vacuum.mqtttest"}
assert not [
record
for record in caplog.records
if record.name == "homeassistant.helpers.frame"
and record.levelno >= logging.WARNING
]
assert (
"mqtt' is setting the battery_level which has been deprecated"
) not in caplog.text
@pytest.mark.parametrize(

View File

@@ -94,7 +94,7 @@ def mock_config_entry_with_reasoning_model(
hass.config_entries.async_update_subentry(
mock_config_entry,
next(iter(mock_config_entry.subentries.values())),
data={CONF_LLM_HASS_API: llm.LLM_API_ASSIST, CONF_CHAT_MODEL: "o4-mini"},
data={CONF_LLM_HASS_API: llm.LLM_API_ASSIST, CONF_CHAT_MODEL: "gpt-5-mini"},
)
return mock_config_entry

View File

@@ -20,6 +20,7 @@ from homeassistant.components.openai_conversation.const import (
CONF_RECOMMENDED,
CONF_TEMPERATURE,
CONF_TOP_P,
CONF_VERBOSITY,
CONF_WEB_SEARCH,
CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_CONTEXT_SIZE,
@@ -302,7 +303,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
(
{
CONF_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pirate",
CONF_PROMPT: "Speak like a pro",
},
{
CONF_TEMPERATURE: 1.0,
@@ -317,7 +318,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
),
{
CONF_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pirate",
CONF_PROMPT: "Speak like a pro",
CONF_TEMPERATURE: 1.0,
CONF_CHAT_MODEL: "o1-pro",
CONF_TOP_P: RECOMMENDED_TOP_P,
@@ -414,35 +415,51 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
( # Case 2: reasoning model
{
CONF_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pro",
CONF_PROMPT: "Speak like a pirate",
CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "o1-pro",
CONF_CHAT_MODEL: "gpt-5",
CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000,
CONF_REASONING_EFFORT: "high",
CONF_REASONING_EFFORT: "low",
CONF_VERBOSITY: "high",
CONF_CODE_INTERPRETER: False,
CONF_WEB_SEARCH: False,
CONF_WEB_SEARCH_CONTEXT_SIZE: "low",
CONF_WEB_SEARCH_USER_LOCATION: False,
},
(
{
CONF_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pro",
CONF_PROMPT: "Speak like a pirate",
},
{
CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "o1-pro",
CONF_CHAT_MODEL: "gpt-5",
CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000,
},
{CONF_REASONING_EFFORT: "high", CONF_CODE_INTERPRETER: False},
{
CONF_REASONING_EFFORT: "minimal",
CONF_CODE_INTERPRETER: False,
CONF_VERBOSITY: "high",
CONF_WEB_SEARCH: False,
CONF_WEB_SEARCH_CONTEXT_SIZE: "low",
CONF_WEB_SEARCH_USER_LOCATION: False,
},
),
{
CONF_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pro",
CONF_PROMPT: "Speak like a pirate",
CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "o1-pro",
CONF_CHAT_MODEL: "gpt-5",
CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000,
CONF_REASONING_EFFORT: "high",
CONF_REASONING_EFFORT: "minimal",
CONF_CODE_INTERPRETER: False,
CONF_VERBOSITY: "high",
CONF_WEB_SEARCH: False,
CONF_WEB_SEARCH_CONTEXT_SIZE: "low",
CONF_WEB_SEARCH_USER_LOCATION: False,
},
),
# Test that old options are removed after reconfiguration
@@ -482,11 +499,13 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
CONF_PROMPT: "Speak like a pirate",
CONF_LLM_HASS_API: ["assist"],
CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "gpt-4o",
CONF_CHAT_MODEL: "gpt-5",
CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000,
CONF_REASONING_EFFORT: "high",
CONF_CODE_INTERPRETER: True,
CONF_VERBOSITY: "low",
CONF_WEB_SEARCH: False,
},
(
{
@@ -550,11 +569,12 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
CONF_PROMPT: "Speak like a pirate",
CONF_LLM_HASS_API: ["assist"],
CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "o3-mini",
CONF_CHAT_MODEL: "o5",
CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000,
CONF_REASONING_EFFORT: "low",
CONF_CODE_INTERPRETER: True,
CONF_VERBOSITY: "medium",
},
(
{

View File

@@ -12,9 +12,9 @@ from tests.common import MockConfigEntry
mock_value_step_user = {
"title": "1R & 1IN Board",
"relay_count": 1,
"input_count": 1,
"is_old": False,
"relays": 1,
"inputs": 1,
"temps": False,
}

View File

@@ -882,3 +882,23 @@ def ungroup_speakers(coordinator: MockSoCo, group_member: MockSoCo) -> None:
)
coordinator.zoneGroupTopology.subscribe.return_value._callback(event)
group_member.zoneGroupTopology.subscribe.return_value._callback(event)
def create_rendering_control_event(
soco: MockSoCo,
) -> SonosMockEvent:
"""Create a Sonos Event for speaker rendering control."""
variables = {
"dialog_level": 1,
"speech_enhance_enable": 1,
"surround_level": 6,
"music_surround_level": 4,
"audio_delay": 0,
"audio_delay_left_rear": 0,
"audio_delay_right_rear": 0,
"night_mode": 0,
"surround_enabled": 1,
"surround_mode": 1,
"height_channel_level": 1,
}
return SonosMockEvent(soco, soco.renderingControl, variables)

View File

@@ -6,13 +6,18 @@ from unittest.mock import patch
import pytest
from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER
from homeassistant.components.sonos.const import (
DATA_SONOS_DISCOVERY_MANAGER,
MODEL_SONOS_ARC_ULTRA,
)
from homeassistant.components.sonos.switch import (
ATTR_DURATION,
ATTR_ID,
ATTR_INCLUDE_LINKED_ZONES,
ATTR_PLAY_MODE,
ATTR_RECURRENCE,
ATTR_SPEECH_ENHANCEMENT,
ATTR_SPEECH_ENHANCEMENT_ENABLED,
ATTR_VOLUME,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
@@ -29,7 +34,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from .conftest import MockSoCo, SonosMockEvent
from .conftest import MockSoCo, SonosMockEvent, create_rendering_control_event
from tests.common import async_fire_time_changed
@@ -142,6 +147,49 @@ async def test_switch_attributes(
assert touch_controls_state.state == STATE_ON
@pytest.mark.parametrize(
("model", "attribute"),
[
("Sonos One SL", ATTR_SPEECH_ENHANCEMENT),
(MODEL_SONOS_ARC_ULTRA.lower(), ATTR_SPEECH_ENHANCEMENT_ENABLED),
],
)
async def test_switch_speech_enhancement(
hass: HomeAssistant,
async_setup_sonos,
soco: MockSoCo,
speaker_info: dict[str, str],
entity_registry: er.EntityRegistry,
model: str,
attribute: str,
) -> None:
"""Tests the speech enhancement switch and attribute substitution for different models."""
entity_id = "switch.zone_a_speech_enhancement"
speaker_info["model_name"] = model
soco.get_speaker_info.return_value = speaker_info
setattr(soco, attribute, True)
await async_setup_sonos()
switch = entity_registry.entities[entity_id]
state = hass.states.get(switch.entity_id)
assert state.state == STATE_ON
event = create_rendering_control_event(soco)
event.variables[attribute] = False
soco.renderingControl.subscribe.return_value._callback(event)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get(switch.entity_id)
assert state.state == STATE_OFF
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert getattr(soco, attribute) is True
@pytest.mark.parametrize(
("service", "expected_result"),
[

View File

@@ -114,6 +114,7 @@ DEVICE_MOCKS = {
"kj_CAjWAxBUZt7QZHfz": [
# https://github.com/home-assistant/core/issues/146023
Platform.FAN,
Platform.SENSOR,
Platform.SWITCH,
],
"kj_yrzylxax1qspdgpp": [

View File

@@ -1649,6 +1649,58 @@
'state': '220.4',
})
# ---
# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][sensor.hl400_pm2_5-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.hl400_pm2_5',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'PM2.5',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'pm25',
'unique_id': 'tuya.152027113c6105cce49cpm25',
'unit_of_measurement': '',
})
# ---
# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][sensor.hl400_pm2_5-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'HL400 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '',
}),
'context': <ANY>,
'entity_id': 'sensor.hl400_pm2_5',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '45.0',
})
# ---
# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -5,9 +5,10 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock, patch
import pytest
from uiprotect import NotAuthorized, NvrError, ProtectApiClient
from uiprotect import NvrError, ProtectApiClient
from uiprotect.api import DEVICE_UPDATE_INTERVAL
from uiprotect.data import NVR, Bootstrap, CloudAccount, Light
from uiprotect.exceptions import BadRequest, NotAuthorized
from homeassistant.components.unifiprotect.const import (
AUTH_RETRIES,
@@ -414,6 +415,28 @@ async def test_setup_handles_api_key_creation_failure(
ufp.api.set_api_key.assert_not_called()
@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True)
async def test_setup_handles_api_key_creation_bad_request(
hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock
) -> None:
"""Test handling of API key creation BadRequest error."""
# Setup: API key is not set, user has write permissions, but creation fails with BadRequest
ufp.api.is_api_key_set.return_value = False
ufp.api.create_api_key = AsyncMock(
side_effect=BadRequest("Invalid API key creation request")
)
# Should fail with auth error due to API key creation failure
await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done()
assert ufp.entry.state is ConfigEntryState.SETUP_ERROR
# Verify API key creation was attempted but set_api_key was not called
ufp.api.create_api_key.assert_called_once_with(name="Home Assistant (test home)")
ufp.api.set_api_key.assert_not_called()
async def test_setup_with_existing_api_key(
hass: HomeAssistant, ufp: MockUFPFixture
) -> None:

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from enum import Enum
import logging
from types import ModuleType
from typing import Any
@@ -437,11 +438,13 @@ async def test_vacuum_deprecated_state_does_not_break_state(
assert state.state == "cleaning"
@pytest.mark.usefixtures("mock_as_custom_component")
async def test_vacuum_log_deprecated_battery_properties(
@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)])
async def test_vacuum_log_deprecated_battery_using_properties(
hass: HomeAssistant,
config_flow_fixture: None,
caplog: pytest.LogCaptureFixture,
is_built_in: bool,
log_warnings: int,
) -> None:
"""Test incorrectly using battery properties logs warning."""
@@ -449,7 +452,7 @@ async def test_vacuum_log_deprecated_battery_properties(
"""Mocked vacuum entity."""
@property
def activity(self) -> str:
def activity(self) -> VacuumActivity:
"""Return the state of the entity."""
return VacuumActivity.CLEANING
@@ -477,7 +480,7 @@ async def test_vacuum_log_deprecated_battery_properties(
async_setup_entry=help_async_setup_entry_init,
async_unload_entry=help_async_unload_entry,
),
built_in=False,
built_in=is_built_in,
)
setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True)
assert await hass.config_entries.async_setup(config_entry.entry_id)
@@ -486,26 +489,27 @@ async def test_vacuum_log_deprecated_battery_properties(
assert state is not None
assert (
"Detected that custom integration 'test' is setting the battery_icon which has been deprecated."
" Integration test should implement a sensor instead with a correct device class and link it"
" to the same device. This will stop working in Home Assistant 2026.8,"
" please report it to the author of the 'test' custom integration"
in caplog.text
len([record for record in caplog.records if record.levelno >= logging.WARNING])
== log_warnings
)
assert (
"Detected that custom integration 'test' is setting the battery_level which has been deprecated."
" Integration test should implement a sensor instead with a correct device class and link it"
" to the same device. This will stop working in Home Assistant 2026.8,"
" please report it to the author of the 'test' custom integration"
"integration 'test' is setting the battery_icon which has been deprecated."
in caplog.text
)
) != is_built_in
assert (
"integration 'test' is setting the battery_level which has been deprecated."
in caplog.text
) != is_built_in
@pytest.mark.usefixtures("mock_as_custom_component")
async def test_vacuum_log_deprecated_battery_properties_using_attr(
@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)])
async def test_vacuum_log_deprecated_battery_using_attr(
hass: HomeAssistant,
config_flow_fixture: None,
caplog: pytest.LogCaptureFixture,
is_built_in: bool,
log_warnings: int,
) -> None:
"""Test incorrectly using _attr_battery_* attribute does log issue and raise repair."""
@@ -531,7 +535,7 @@ async def test_vacuum_log_deprecated_battery_properties_using_attr(
async_setup_entry=help_async_setup_entry_init,
async_unload_entry=help_async_unload_entry,
),
built_in=False,
built_in=is_built_in,
)
setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True)
assert await hass.config_entries.async_setup(config_entry.entry_id)
@@ -541,47 +545,51 @@ async def test_vacuum_log_deprecated_battery_properties_using_attr(
entity.start()
assert (
"Detected that custom integration 'test' is setting the battery_level which has been deprecated."
" Integration test should implement a sensor instead with a correct device class and link it to"
" the same device. This will stop working in Home Assistant 2026.8,"
" please report it to the author of the 'test' custom integration"
in caplog.text
len([record for record in caplog.records if record.levelno >= logging.WARNING])
== log_warnings
)
assert (
"Detected that custom integration 'test' is setting the battery_icon which has been deprecated."
" Integration test should implement a sensor instead with a correct device class and link it to"
" the same device. This will stop working in Home Assistant 2026.8,"
" please report it to the author of the 'test' custom integration"
"integration 'test' is setting the battery_level which has been deprecated."
in caplog.text
)
) != is_built_in
assert (
"integration 'test' is setting the battery_icon which has been deprecated."
in caplog.text
) != is_built_in
await async_start(hass, entity.entity_id)
caplog.clear()
await async_start(hass, entity.entity_id)
# Test we only log once
assert (
"Detected that custom integration 'test' is setting the battery_level which has been deprecated."
not in caplog.text
)
assert (
"Detected that custom integration 'test' is setting the battery_icon which has been deprecated."
not in caplog.text
len([record for record in caplog.records if record.levelno >= logging.WARNING])
== 0
)
@pytest.mark.usefixtures("mock_as_custom_component")
@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 1)])
async def test_vacuum_log_deprecated_battery_supported_feature(
hass: HomeAssistant,
config_flow_fixture: None,
caplog: pytest.LogCaptureFixture,
is_built_in: bool,
log_warnings: int,
) -> None:
"""Test incorrectly setting battery supported feature logs warning."""
entity = MockVacuum(
name="Testing",
entity_id="vacuum.test",
)
class MockVacuum(StateVacuumEntity):
"""Mock vacuum class."""
_attr_supported_features = (
VacuumEntityFeature.STATE | VacuumEntityFeature.BATTERY
)
_attr_name = "Testing"
entity = MockVacuum()
config_entry = MockConfigEntry(domain="test")
config_entry.add_to_hass(hass)
@@ -592,7 +600,7 @@ async def test_vacuum_log_deprecated_battery_supported_feature(
async_setup_entry=help_async_setup_entry_init,
async_unload_entry=help_async_unload_entry,
),
built_in=False,
built_in=is_built_in,
)
setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True)
assert await hass.config_entries.async_setup(config_entry.entry_id)
@@ -601,13 +609,14 @@ async def test_vacuum_log_deprecated_battery_supported_feature(
assert state is not None
assert (
"Detected that custom integration 'test' is setting the battery supported feature"
" which has been deprecated. Integration test should remove this as part of migrating"
" the battery level and icon to a sensor. This will stop working in Home Assistant 2026.8"
", please report it to the author of the 'test' custom integration"
in caplog.text
len([record for record in caplog.records if record.levelno >= logging.WARNING])
== log_warnings
)
assert (
"integration 'test' is setting the battery supported feature" in caplog.text
) != is_built_in
async def test_vacuum_not_log_deprecated_battery_properties_during_init(
hass: HomeAssistant,
@@ -624,7 +633,7 @@ async def test_vacuum_not_log_deprecated_battery_properties_during_init(
self._attr_battery_level = 50
@property
def activity(self) -> str:
def activity(self) -> VacuumActivity:
"""Return the state of the entity."""
return VacuumActivity.CLEANING
@@ -635,6 +644,6 @@ async def test_vacuum_not_log_deprecated_battery_properties_during_init(
assert entity.battery_level == 50
assert (
"Detected that custom integration 'test' is setting the battery_level which has been deprecated."
not in caplog.text
len([record for record in caplog.records if record.levelno >= logging.WARNING])
== 0
)

View File

@@ -20,6 +20,12 @@ _MODEL_SPECIFIC_RESPONSES = {
"statistics",
"vehicle",
],
"xc60_phev_2020": [
"energy_capabilities",
"energy_state",
"statistics",
"vehicle",
],
"xc90_petrol_2019": ["commands", "statistics", "vehicle"],
}

View File

@@ -1,57 +1,56 @@
{
"batteryChargeLevel": {
"status": "OK",
"value": 38,
"value": 90.0,
"unit": "percentage",
"updatedAt": "2025-07-02T08:51:23Z"
"updatedAt": "2025-08-07T14:30:32Z"
},
"electricRange": {
"status": "OK",
"value": 90,
"value": 327,
"unit": "km",
"updatedAt": "2025-07-02T08:51:23Z"
"updatedAt": "2025-08-07T14:30:32Z"
},
"chargerConnectionStatus": {
"status": "OK",
"value": "DISCONNECTED",
"updatedAt": "2025-07-02T08:51:23Z"
"value": "CONNECTED",
"updatedAt": "2025-08-07T14:30:32Z"
},
"chargingStatus": {
"status": "OK",
"value": "IDLE",
"updatedAt": "2025-07-02T08:51:23Z"
"value": "DONE",
"updatedAt": "2025-08-07T14:30:32Z"
},
"chargingType": {
"status": "OK",
"value": "NONE",
"updatedAt": "2025-07-02T08:51:23Z"
"value": "AC",
"updatedAt": "2025-08-07T14:30:32Z"
},
"chargerPowerStatus": {
"status": "OK",
"value": "NO_POWER_AVAILABLE",
"updatedAt": "2025-07-02T08:51:23Z"
"value": "FAULT",
"updatedAt": "2025-08-07T14:30:32Z"
},
"estimatedChargingTimeToTargetBatteryChargeLevel": {
"status": "OK",
"value": 0,
"value": 2,
"unit": "minutes",
"updatedAt": "2025-07-02T08:51:23Z"
"updatedAt": "2025-08-07T14:30:32Z"
},
"chargingCurrentLimit": {
"status": "OK",
"value": 32,
"unit": "ampere",
"updatedAt": "2024-03-05T08:38:44Z"
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
},
"targetBatteryChargeLevel": {
"status": "OK",
"value": 90,
"unit": "percentage",
"updatedAt": "2024-09-22T09:40:12Z"
"updatedAt": "2025-08-07T14:49:50Z"
},
"chargingPower": {
"status": "ERROR",
"code": "PROPERTY_NOT_FOUND",
"message": "No valid value could be found for the requested property"
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
}
}

View File

@@ -7,8 +7,8 @@
},
"electricRange": {
"status": "OK",
"value": 220,
"unit": "km",
"value": 150,
"unit": "mi",
"updatedAt": "2025-07-02T08:51:23Z"
},
"chargerConnectionStatus": {

View File

@@ -0,0 +1,33 @@
{
"isSupported": true,
"batteryChargeLevel": {
"isSupported": false
},
"electricRange": {
"isSupported": false
},
"chargerConnectionStatus": {
"isSupported": true
},
"chargingSystemStatus": {
"isSupported": true
},
"chargingType": {
"isSupported": false
},
"chargerPowerStatus": {
"isSupported": false
},
"estimatedChargingTimeToTargetBatteryChargeLevel": {
"isSupported": false
},
"targetBatteryChargeLevel": {
"isSupported": true
},
"chargingCurrentLimit": {
"isSupported": false
},
"chargingPower": {
"isSupported": false
}
}

View File

@@ -0,0 +1,52 @@
{
"batteryChargeLevel": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
},
"electricRange": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
},
"chargerConnectionStatus": {
"status": "OK",
"value": "DISCONNECTED",
"updatedAt": "2025-08-07T20:29:18Z"
},
"chargingStatus": {
"status": "OK",
"value": "IDLE",
"updatedAt": "2025-08-07T20:29:18Z"
},
"chargingType": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
},
"chargerPowerStatus": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
},
"estimatedChargingTimeToTargetBatteryChargeLevel": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
},
"chargingCurrentLimit": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
},
"targetBatteryChargeLevel": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
},
"chargingPower": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
}
}

View File

@@ -0,0 +1,32 @@
{
"averageFuelConsumption": {
"value": 4.0,
"unit": "l/100km",
"timestamp": "2025-08-07T20:29:18.343Z"
},
"averageSpeed": {
"value": 65,
"unit": "km/h",
"timestamp": "2025-08-07T20:29:18.343Z"
},
"tripMeterManual": {
"value": 219.7,
"unit": "km",
"timestamp": "2025-08-07T20:29:18.343Z"
},
"tripMeterAutomatic": {
"value": 0.0,
"unit": "km",
"timestamp": "2025-08-07T20:29:18.343Z"
},
"distanceToEmptyTank": {
"value": 920,
"unit": "km",
"timestamp": "2025-08-07T20:29:18.343Z"
},
"distanceToEmptyBattery": {
"value": 29,
"unit": "km",
"timestamp": "2025-08-07T20:29:18.343Z"
}
}

View File

@@ -0,0 +1,17 @@
{
"vin": "YV1ABCDEFG1234567",
"modelYear": 2020,
"gearbox": "AUTOMATIC",
"fuelType": "PETROL/ELECTRIC",
"externalColour": "Bright Silver",
"batteryCapacityKWH": 11.832,
"images": {
"exteriorImageUrl": "https://cas.volvocars.com/image/dynamic/MY20_0000/123/exterior-v1/_/default.png?market=se&client=public-api-engineering&angle=1&bg=00000000&w=1920",
"internalImageUrl": "https://cas.volvocars.com/image/dynamic/MY20_0000/123/interior-v1/_/default.jpg?market=se&client=public-api-engineering&angle=0&w=1920"
},
"descriptions": {
"model": "XC60",
"upholstery": "CHARCOAL/LEABR3/CHARC/SPO",
"steering": "LEFT"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,13 @@ from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.parametrize(
"full_model",
["ex30_2024", "s90_diesel_2018", "xc40_electric_2024", "xc90_petrol_2019"],
[
"ex30_2024",
"s90_diesel_2018",
"xc40_electric_2024",
"xc60_phev_2020",
"xc90_petrol_2019",
],
)
async def test_sensor(
hass: HomeAssistant,
@@ -30,3 +36,36 @@ async def test_sensor(
assert await setup_integration()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
"full_model",
["xc40_electric_2024"],
)
async def test_distance_to_empty_battery(
hass: HomeAssistant,
setup_integration: Callable[[], Awaitable[bool]],
) -> None:
"""Test using `distanceToEmptyBattery` instead of `electricRange`."""
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
assert await setup_integration()
assert hass.states.get("sensor.volvo_xc40_distance_to_empty_battery").state == "250"
@pytest.mark.parametrize(
("full_model", "short_model"),
[("ex30_2024", "ex30"), ("xc60_phev_2020", "xc60")],
)
async def test_skip_invalid_api_fields(
hass: HomeAssistant,
setup_integration: Callable[[], Awaitable[bool]],
short_model: str,
) -> None:
"""Test if invalid values are not creating a sensor."""
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
assert await setup_integration()
assert not hass.states.get(f"sensor.volvo_{short_model}_charging_power")

View File

@@ -36,6 +36,7 @@
}),
'network_key': '**REDACTED**',
'nwk_addresses': dict({
'11:22:33:44:55:66:77:88': 4660,
}),
'nwk_manager_id': 0,
'nwk_update_id': 0,

View File

@@ -6,6 +6,7 @@ import pytest
from syrupy.assertion import SnapshotAssertion
from syrupy.filters import props
from zigpy.profiles import zha
from zigpy.types import EUI64, NWK
from zigpy.zcl.clusters import security
from homeassistant.components.zha.helpers import (
@@ -71,6 +72,10 @@ async def test_diagnostics_for_config_entry(
gateway.application_controller.energy_scan.side_effect = None
gateway.application_controller.energy_scan.return_value = scan
gateway.application_controller.state.network_info.nwk_addresses = {
EUI64.convert("11:22:33:44:55:66:77:88"): NWK(0x1234)
}
diagnostics_data = await get_diagnostics_for_config_entry(
hass, hass_client, config_entry
)

View File

@@ -28,7 +28,13 @@ from homeassistant.components.zwave_js.discovery_data_template import (
DynamicCurrentTempClimateDataTemplate,
)
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, EntityCategory
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_OFF,
STATE_UNKNOWN,
EntityCategory,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import dt as dt_util
@@ -253,6 +259,7 @@ async def test_merten_507801_disabled_enitites(
assert updated_entry.disabled is False
@pytest.mark.parametrize("platforms", [[Platform.BUTTON, Platform.NUMBER]])
async def test_zooz_zen72(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
@@ -324,6 +331,9 @@ async def test_zooz_zen72(
assert args["value"] is True
@pytest.mark.parametrize(
"platforms", [[Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]]
)
async def test_indicator_test(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -167,7 +167,7 @@ async def test_update_entity_states(
client.async_send_command.return_value = {"updates": []}
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
@@ -186,7 +186,7 @@ async def test_update_entity_states(
client.async_send_command.return_value = FIRMWARE_UPDATES
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=2))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
@@ -224,7 +224,7 @@ async def test_update_entity_states(
client.async_send_command.return_value = {"updates": []}
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=3))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=3))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
@@ -246,7 +246,7 @@ async def test_update_entity_install_raises(
"""Test update entity install raises exception."""
client.async_send_command.return_value = FIRMWARE_UPDATES
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
await hass.async_block_till_done()
# Test failed installation by driver
@@ -279,7 +279,7 @@ async def test_update_entity_sleep(
client.async_send_command.return_value = FIRMWARE_UPDATES
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
await hass.async_block_till_done()
# Two nodes in total, the controller node and the zen_31 node.
@@ -324,7 +324,7 @@ async def test_update_entity_dead(
client.async_send_command.return_value = FIRMWARE_UPDATES
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
await hass.async_block_till_done()
# Two nodes in total, the controller node and the zen_31 node.
@@ -368,14 +368,14 @@ async def test_update_entity_ha_not_running(
# Update should be delayed by a day because Home Assistant is not running
hass.set_state(CoreState.starting)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15))
await hass.async_block_till_done()
assert client.async_send_command.call_count == 0
hass.set_state(CoreState.running)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
await hass.async_block_till_done()
# Two nodes in total, the controller node and the zen_31 node.
@@ -401,7 +401,7 @@ async def test_update_entity_update_failure(
assert client.async_send_command.call_count == 0
client.async_send_command.side_effect = FailedZWaveCommand("test", 260, "test")
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
await hass.async_block_till_done()
entity_ids = (CONTROLLER_UPDATE_ENTITY, NODE_UPDATE_ENTITY)
@@ -509,7 +509,7 @@ async def test_update_entity_progress(
client.async_send_command.return_value = FIRMWARE_UPDATES
driver = client.driver
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
@@ -657,7 +657,7 @@ async def test_update_entity_install_failed(
driver = client.driver
client.async_send_command.return_value = FIRMWARE_UPDATES
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
@@ -733,7 +733,7 @@ async def test_update_entity_reload(
client.async_send_command.return_value = {"updates": []}
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
@@ -742,7 +742,7 @@ async def test_update_entity_reload(
client.async_send_command.return_value = FIRMWARE_UPDATES
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=2))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
@@ -774,7 +774,7 @@ async def test_update_entity_reload(
await hass.async_block_till_done()
# Trigger another update and make sure the skipped version is still skipped
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=4))
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=4))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
@@ -809,7 +809,7 @@ async def test_update_entity_delay(
assert client.async_send_command.call_count == 0
update_interval = timedelta(minutes=5)
update_interval = timedelta(seconds=15)
freezer.tick(update_interval)
async_fire_time_changed(hass)
await hass.async_block_till_done()