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", "documentation": "https://www.home-assistant.io/integrations/airos",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["airos==0.2.4"] "requirements": ["airos==0.2.7"]
} }

View File

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

View File

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

View File

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

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "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, ApiError,
AuthError, AuthError,
HusqvarnaTimeoutError, HusqvarnaTimeoutError,
HusqvarnaWSClientError,
HusqvarnaWSServerHandshakeError, HusqvarnaWSServerHandshakeError,
) )
from aioautomower.model import MowerDictionary from aioautomower.model import MowerDictionary
@@ -142,7 +143,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
# Reset reconnect time after successful connection # Reset reconnect time after successful connection
self.reconnect_time = DEFAULT_RECONNECT_TIME self.reconnect_time = DEFAULT_RECONNECT_TIME
await automower_client.start_listening() await automower_client.start_listening()
except HusqvarnaWSServerHandshakeError as err: except (HusqvarnaWSServerHandshakeError, HusqvarnaWSClientError) as err:
_LOGGER.debug( _LOGGER.debug(
"Failed to connect to websocket. Trying to reconnect: %s", "Failed to connect to websocket. Trying to reconnect: %s",
err, err,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -186,6 +186,9 @@ MODELS_TV_ONLY = (
"ULTRA", "ULTRA",
) )
MODELS_LINEIN_AND_TV = ("AMP",) 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_CHECK_INTERVAL = datetime.timedelta(minutes=1)
AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5 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 .alarms import SonosAlarms
from .const import ( from .const import (
ATTR_SPEECH_ENHANCEMENT_ENABLED,
AVAILABILITY_TIMEOUT, AVAILABILITY_TIMEOUT,
BATTERY_SCAN_INTERVAL, BATTERY_SCAN_INTERVAL,
DOMAIN, DOMAIN,
@@ -157,6 +158,7 @@ class SonosSpeaker:
# Home theater # Home theater
self.audio_delay: int | None = None self.audio_delay: int | None = None
self.dialog_level: bool | None = None self.dialog_level: bool | None = None
self.speech_enhance_enabled: bool | None = None
self.night_mode: bool | None = None self.night_mode: bool | None = None
self.sub_enabled: bool | None = None self.sub_enabled: bool | None = None
self.sub_crossover: int | None = None self.sub_crossover: int | None = None
@@ -548,6 +550,11 @@ class SonosSpeaker:
@callback @callback
def async_update_volume(self, event: SonosEvent) -> None: def async_update_volume(self, event: SonosEvent) -> None:
"""Update information about currently volume settings.""" """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) self.event_stats.process(event)
variables = event.variables variables = event.variables
@@ -565,6 +572,7 @@ class SonosSpeaker:
for bool_var in ( for bool_var in (
"dialog_level", "dialog_level",
ATTR_SPEECH_ENHANCEMENT_ENABLED,
"night_mode", "night_mode",
"sub_enabled", "sub_enabled",
"surround_enabled", "surround_enabled",

View File

@@ -19,7 +19,9 @@ from homeassistant.helpers.event import async_track_time_change
from .alarms import SonosAlarms from .alarms import SonosAlarms
from .const import ( from .const import (
ATTR_SPEECH_ENHANCEMENT_ENABLED,
DOMAIN, DOMAIN,
MODEL_SONOS_ARC_ULTRA,
SONOS_ALARMS_UPDATED, SONOS_ALARMS_UPDATED,
SONOS_CREATE_ALARM, SONOS_CREATE_ALARM,
SONOS_CREATE_SWITCHES, SONOS_CREATE_SWITCHES,
@@ -59,6 +61,7 @@ ALL_FEATURES = (
ATTR_SURROUND_ENABLED, ATTR_SURROUND_ENABLED,
ATTR_STATUS_LIGHT, ATTR_STATUS_LIGHT,
) )
ALL_SUBST_FEATURES = (ATTR_SPEECH_ENHANCEMENT_ENABLED,)
COORDINATOR_FEATURES = ATTR_CROSSFADE COORDINATOR_FEATURES = ATTR_CROSSFADE
@@ -69,6 +72,14 @@ POLL_REQUIRED = (
WEEKEND_DAYS = (0, 6) 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -92,6 +103,13 @@ async def async_setup_entry(
def available_soco_attributes(speaker: SonosSpeaker) -> list[str]: def available_soco_attributes(speaker: SonosSpeaker) -> list[str]:
features = [] 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: for feature_type in ALL_FEATURES:
try: try:
if (state := getattr(speaker.soco, feature_type, None)) is not None: if (state := getattr(speaker.soco, feature_type, None)) is not None:
@@ -107,12 +125,23 @@ async def async_setup_entry(
available_soco_attributes, speaker available_soco_attributes, speaker
) )
for feature_type in available_features: for feature_type in available_features:
attribute_key = MODEL_FEATURE_SUBSTITUTIONS.get(
speaker.model_name.upper(), {}
).get(feature_type, feature_type)
_LOGGER.debug( _LOGGER.debug(
"Creating %s switch on %s", "Creating %s switch on %s attribute %s",
feature_type, feature_type,
speaker.zone_name, 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) async_add_entities(entities)
config_entry.async_on_unload( config_entry.async_on_unload(
@@ -127,11 +156,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
"""Representation of a Sonos feature switch.""" """Representation of a Sonos feature switch."""
def __init__( def __init__(
self, feature_type: str, speaker: SonosSpeaker, config_entry: SonosConfigEntry self,
feature_type: str,
attribute_key: str,
speaker: SonosSpeaker,
config_entry: SonosConfigEntry,
) -> None: ) -> None:
"""Initialize the switch.""" """Initialize the switch."""
super().__init__(speaker, config_entry) super().__init__(speaker, config_entry)
self.feature_type = feature_type self.attribute_key = attribute_key
self.needs_coordinator = feature_type in COORDINATOR_FEATURES self.needs_coordinator = feature_type in COORDINATOR_FEATURES
self._attr_entity_category = EntityCategory.CONFIG self._attr_entity_category = EntityCategory.CONFIG
self._attr_translation_key = feature_type self._attr_translation_key = feature_type
@@ -149,15 +182,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
@soco_error() @soco_error()
def poll_state(self) -> None: def poll_state(self) -> None:
"""Poll the current state of the switch.""" """Poll the current state of the switch."""
state = getattr(self.soco, self.feature_type) state = getattr(self.soco, self.attribute_key)
setattr(self.speaker, self.feature_type, state) setattr(self.speaker, self.attribute_key, state)
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if entity is on.""" """Return True if entity is on."""
if self.needs_coordinator and not self.speaker.is_coordinator: 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.coordinator, self.attribute_key))
return cast(bool, getattr(self.speaker, self.feature_type)) return cast(bool, getattr(self.speaker, self.attribute_key))
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on.""" """Turn the entity on."""
@@ -175,7 +208,7 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
else: else:
soco = self.soco soco = self.soco
try: try:
setattr(soco, self.feature_type, enable) setattr(soco, self.attribute_key, enable)
except SoCoUPnPException as exc: except SoCoUPnPException as exc:
_LOGGER.warning("Could not toggle %s: %s", self.entity_id, 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] cmd = ["apps", 0, browse_limit]
result = await player.async_query(*cmd) result = await player.async_query(*cmd)
for app in result["appss_loop"]: if result["appss_loop"]:
app_cmd = "app-" + app["cmd"] for app in result["appss_loop"]:
if app_cmd not in self.known_apps_radios: app_cmd = "app-" + app["cmd"]
self.add_new_command(app_cmd, "item_id") if app_cmd not in self.known_apps_radios:
_LOGGER.debug( self.add_new_command(app_cmd, "item_id")
"Adding new command %s to browse data for player %s", _LOGGER.debug(
app_cmd, "Adding new command %s to browse data for player %s",
player.player_id, app_cmd,
) player.player_id,
)
cmd = ["radios", 0, browse_limit] cmd = ["radios", 0, browse_limit]
result = await player.async_query(*cmd) result = await player.async_query(*cmd)
for app in result["radioss_loop"]: if result["radioss_loop"]:
app_cmd = "app-" + app["cmd"] for app in result["radioss_loop"]:
if app_cmd not in self.known_apps_radios: app_cmd = "app-" + app["cmd"]
self.add_new_command(app_cmd, "item_id") if app_cmd not in self.known_apps_radios:
_LOGGER.debug( self.add_new_command(app_cmd, "item_id")
"Adding new command %s to browse data for player %s", _LOGGER.debug(
app_cmd, "Adding new command %s to browse data for player %s",
player.player_id, app_cmd,
) player.player_id,
)
def _build_response_apps_radios_category( def _build_response_apps_radios_category(

View File

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

View File

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

View File

@@ -99,8 +99,22 @@ class EnumTypeData:
return cls(dpcode, **parsed) 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 @dataclass
class ElectricityTypeData: class ElectricityTypeData(ComplexTypeData):
"""Electricity Type Data.""" """Electricity Type Data."""
electriccurrent: str | None = None electriccurrent: str | None = None
@@ -113,9 +127,11 @@ class ElectricityTypeData:
return cls(**json.loads(data.lower())) return cls(**json.loads(data.lower()))
@classmethod @classmethod
def from_raw(cls, data: str) -> Self: def from_raw(cls, data: str) -> Self | None:
"""Decode base64 string and return a ElectricityTypeData object.""" """Decode base64 string and return a ElectricityTypeData object."""
raw = base64.b64decode(data) raw = base64.b64decode(data)
if len(raw) == 0:
return None
voltage = struct.unpack(">H", raw[0:2])[0] / 10.0 voltage = struct.unpack(">H", raw[0:2])[0] / 10.0
electriccurrent = struct.unpack(">L", b"\x00" + raw[2:5])[0] / 1000.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 power = struct.unpack(">L", b"\x00" + raw[5:8])[0] / 1000.0

View File

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

View File

@@ -8,7 +8,7 @@ import logging
from aiohttp.client_exceptions import ServerDisconnectedError from aiohttp.client_exceptions import ServerDisconnectedError
from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.api import DEVICE_UPDATE_INTERVAL
from uiprotect.data import Bootstrap 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 # Import the test_util.anonymize module from the uiprotect package
# in __init__ to ensure it gets imported in the executor since the # 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( new_api_key = await protect.create_api_key(
name=f"Home Assistant ({hass.config.location_name})" 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) _LOGGER.error("Failed to create API key: %s", err)
else: else:
protect.set_api_key(new_api_key) 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}_" f"{registry_entry.config_entry_id}_"
).removesuffix(f"_{registry_entry.translation_key}") ).removesuffix(f"_{registry_entry.translation_key}")
if monitor := next( 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( entity_registry.async_update_entity(
registry_entry.entity_id, 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_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2026.1")
_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1") _DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1")
_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",) _BATTERY_DEPRECATION_IGNORED_PLATFORMS = (
"mqtt",
"template",
)
class VacuumEntityFeature(IntFlag): class VacuumEntityFeature(IntFlag):
@@ -333,7 +336,7 @@ class StateVacuumEntity(
f"is setting the {property} which has been deprecated." f"is setting the {property} which has been deprecated."
f" Integration {self.platform.platform_name} should implement a sensor" f" Integration {self.platform.platform_name} should implement a sensor"
" instead with a correct device class and link it to the same device", " 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, custom_integration_behavior=ReportBehavior.LOG,
breaks_in_ha_version="2026.8", breaks_in_ha_version="2026.8",
integration_domain=self.platform.platform_name, 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" f" Integration {self.platform.platform_name} should remove this as part of migrating"
" the battery level and icon to a sensor", " the battery level and icon to a sensor",
core_behavior=ReportBehavior.LOG, core_behavior=ReportBehavior.LOG,
core_integration_behavior=ReportBehavior.LOG, core_integration_behavior=ReportBehavior.IGNORE,
custom_integration_behavior=ReportBehavior.LOG, custom_integration_behavior=ReportBehavior.LOG,
breaks_in_ha_version="2026.8", breaks_in_ha_version="2026.8",
integration_domain=self.platform.platform_name, integration_domain=self.platform.platform_name,

View File

@@ -15,6 +15,7 @@ from volvocarsapi.models import (
VolvoAuthException, VolvoAuthException,
VolvoCarsApiBaseModel, VolvoCarsApiBaseModel,
VolvoCarsValue, VolvoCarsValue,
VolvoCarsValueStatusField,
VolvoCarsVehicle, VolvoCarsVehicle,
) )
@@ -36,6 +37,16 @@ type VolvoConfigEntry = ConfigEntry[tuple[VolvoBaseCoordinator, ...]]
type CoordinatorData = dict[str, VolvoCarsApiBaseModel | None] 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]): class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]):
"""Volvo base coordinator.""" """Volvo base coordinator."""
@@ -121,7 +132,13 @@ class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]):
translation_key="update_failed", translation_key="update_failed",
) from result ) 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 valid = True
# Raise an error if not a single API call succeeded # Raise an error if not a single API call succeeded

View File

@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, replace from dataclasses import dataclass
import logging import logging
from typing import Any, cast from typing import Any, cast
@@ -47,7 +47,6 @@ _LOGGER = logging.getLogger(__name__)
class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription): class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription):
"""Describes a Volvo sensor entity.""" """Describes a Volvo sensor entity."""
source_fields: list[str] | None = None
value_fn: Callable[[VolvoCarsValue], Any] | None = None value_fn: Callable[[VolvoCarsValue], Any] | None = None
@@ -87,7 +86,12 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None:
return 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, ...] = ( _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
# command-accessibility endpoint # command-accessibility endpoint
@@ -110,6 +114,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageEnergyConsumption", api_field="averageEnergyConsumption",
native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
), ),
# statistics endpoint # statistics endpoint
VolvoSensorDescription( VolvoSensorDescription(
@@ -117,6 +122,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageEnergyConsumptionAutomatic", api_field="averageEnergyConsumptionAutomatic",
native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
), ),
# statistics endpoint # statistics endpoint
VolvoSensorDescription( VolvoSensorDescription(
@@ -124,6 +130,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageEnergyConsumptionSinceCharge", api_field="averageEnergyConsumptionSinceCharge",
native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
), ),
# statistics endpoint # statistics endpoint
VolvoSensorDescription( VolvoSensorDescription(
@@ -131,6 +138,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageFuelConsumption", api_field="averageFuelConsumption",
native_unit_of_measurement="L/100 km", native_unit_of_measurement="L/100 km",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
), ),
# statistics endpoint # statistics endpoint
VolvoSensorDescription( VolvoSensorDescription(
@@ -138,6 +146,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
api_field="averageFuelConsumptionAutomatic", api_field="averageFuelConsumptionAutomatic",
native_unit_of_measurement="L/100 km", native_unit_of_measurement="L/100 km",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
), ),
# statistics endpoint # statistics endpoint
VolvoSensorDescription( VolvoSensorDescription(
@@ -235,11 +244,15 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
"none", "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( VolvoSensorDescription(
key="distance_to_empty_battery", key="distance_to_empty_battery",
api_field="", api_field="distanceToEmptyBattery",
source_fields=["distanceToEmptyBattery", "electricRange"],
native_unit_of_measurement=UnitOfLength.KILOMETERS, native_unit_of_measurement=UnitOfLength.KILOMETERS,
device_class=SensorDeviceClass.DISTANCE, device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
@@ -357,12 +370,7 @@ async def async_setup_entry(
if description.key in added_keys: if description.key in added_keys:
continue continue
if description.source_fields: if description.api_field in coordinator.data:
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:
_add_entity(coordinator, description) _add_entity(coordinator, description)
async_add_entities(entities) async_add_entities(entities)

View File

@@ -94,7 +94,7 @@
"state": { "state": {
"connected": "[%key:common::state::connected%]", "connected": "[%key:common::state::connected%]",
"disconnected": "[%key:common::state::disconnected%]", "disconnected": "[%key:common::state::disconnected%]",
"fault": "[%key:common::state::error%]" "fault": "[%key:common::state::fault%]"
} }
}, },
"charging_current_limit": { "charging_current_limit": {
@@ -106,6 +106,8 @@
"charging_power_status": { "charging_power_status": {
"name": "Charging power status", "name": "Charging power status",
"state": { "state": {
"fault": "[%key:common::state::fault%]",
"power_available_but_not_activated": "Power available",
"providing_power": "Providing power", "providing_power": "Providing power",
"no_power_available": "No 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.const import ATTR_IEEE
from zha.application.gateway import Gateway from zha.application.gateway import Gateway
from zigpy.application import ControllerApplication
from zigpy.config import CONF_NWK_EXTENDED_PAN_ID from zigpy.config import CONF_NWK_EXTENDED_PAN_ID
from zigpy.types import Channels from zigpy.types import Channels
@@ -63,6 +64,19 @@ def shallow_asdict(obj: Any) -> dict:
return obj 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( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -79,7 +93,7 @@ async def async_get_config_entry_diagnostics(
{ {
"config": zha_data.yaml_config, "config": zha_data.yaml_config,
"config_entry": config_entry.as_dict(), "config_entry": config_entry.as_dict(),
"application_state": shallow_asdict(app.state), "application_state": get_application_state_diagnostics(app),
"energy_scan": { "energy_scan": {
channel: 100 * energy / 255 for channel, energy in energy_scan.items() channel: 100 * energy / 255 for channel, energy in energy_scan.items()
}, },

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025 MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 8 MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) 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 go2rtc-client==0.2.1
ha-ffmpeg==3.2.2 ha-ffmpeg==3.2.2
habluetooth==4.0.2 habluetooth==4.0.2
hass-nabucasa==0.111.1 hass-nabucasa==0.111.2
hassil==2.2.3 hassil==2.2.3
home-assistant-bluetooth==1.13.1 home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250806.0 home-assistant-frontend==20250811.0
home-assistant-intents==2025.7.30 home-assistant-intents==2025.7.30
httpx==0.28.1 httpx==0.28.1
ifaddr==0.2.0 ifaddr==0.2.0
@@ -213,3 +213,6 @@ multidict>=6.4.2
# Stable Alpine current only ships cargo 1.83.0 # Stable Alpine current only ships cargo 1.83.0
# No wheels upstream available for armhf & armv7 # No wheels upstream available for armhf & armv7
rpds-py==0.24.0 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] [project]
name = "homeassistant" name = "homeassistant"
version = "2025.8.0" version = "2025.8.1"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
@@ -47,7 +47,7 @@ dependencies = [
"fnv-hash-fast==1.5.0", "fnv-hash-fast==1.5.0",
# hass-nabucasa is imported by helpers which don't depend on the cloud # hass-nabucasa is imported by helpers which don't depend on the cloud
# integration # integration
"hass-nabucasa==0.111.1", "hass-nabucasa==0.111.2",
# When bumping httpx, please check the version pins of # When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all # httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.28.1", "httpx==0.28.1",

2
requirements.txt generated
View File

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

12
requirements_all.txt generated
View File

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

View File

@@ -435,7 +435,7 @@ airgradient==0.9.2
airly==1.1.0 airly==1.1.0
# homeassistant.components.airos # homeassistant.components.airos
airos==0.2.4 airos==0.2.7
# homeassistant.components.airthings_ble # homeassistant.components.airthings_ble
airthings-ble==0.9.2 airthings-ble==0.9.2
@@ -994,7 +994,7 @@ habiticalib==0.4.1
habluetooth==4.0.2 habluetooth==4.0.2
# homeassistant.components.cloud # homeassistant.components.cloud
hass-nabucasa==0.111.1 hass-nabucasa==0.111.2
# homeassistant.components.assist_satellite # homeassistant.components.assist_satellite
# homeassistant.components.conversation # homeassistant.components.conversation
@@ -1023,7 +1023,7 @@ hole==0.9.0
holidays==0.78 holidays==0.78
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250806.0 home-assistant-frontend==20250811.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2025.7.30 home-assistant-intents==2025.7.30
@@ -1074,7 +1074,7 @@ igloohome-api==0.1.1
imeon_inverter_api==0.3.14 imeon_inverter_api==0.3.14
# homeassistant.components.imgw_pib # homeassistant.components.imgw_pib
imgw_pib==1.5.2 imgw_pib==1.5.3
# homeassistant.components.incomfort # homeassistant.components.incomfort
incomfort-client==0.6.9 incomfort-client==0.6.9
@@ -1129,7 +1129,7 @@ kegtron-ble==0.4.0
knocki==0.4.2 knocki==0.4.2
# homeassistant.components.knx # homeassistant.components.knx
knx-frontend==2025.8.6.52906 knx-frontend==2025.8.9.63154
# homeassistant.components.konnected # homeassistant.components.konnected
konnected==1.2.0 konnected==1.2.0
@@ -2647,7 +2647,7 @@ zeroconf==0.147.0
zeversolar==0.3.2 zeversolar==0.3.2
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.66 zha==0.0.68
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.67.1 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 # Stable Alpine current only ships cargo 1.83.0
# No wheels upstream available for armhf & armv7 # No wheels upstream available for armhf & armv7
rpds-py==0.24.0 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 = ( GENERATED_MESSAGE = (

View File

@@ -15,7 +15,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture
@pytest.fixture @pytest.fixture
def ap_fixture(): def ap_fixture():
"""Load fixture data for AP mode.""" """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) return AirOSData.from_dict(json_data)

View File

@@ -1,132 +1,194 @@
{ {
"chain_names": [ "chain_names": [
{ "number": 1, "name": "Chain 0" }, {
{ "number": 2, "name": "Chain 1" } "name": "Chain 0",
"number": 1
},
{
"name": "Chain 1",
"number": 2
}
], ],
"host": { "derived": {
"hostname": "NanoStation 5AC ap name", "access_point": true,
"device_id": "03aa0d0b40fed0a47088293584ef5432", "mac": "01:23:45:67:89:AB",
"uptime": 264888, "mac_interface": "br0",
"power_time": 268683, "ptmp": false,
"time": "2025-06-23 23:06:42", "ptp": true,
"timestamp": 2668313184, "station": false
"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
}, },
"firewall": { "firewall": {
"iptables": false, "eb6tables": false,
"ebtables": false, "ebtables": false,
"ip6tables": 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, "portfw": false,
"provmode": {},
"services": {
"airview": 2,
"dhcp6d_stateful": false,
"dhcpc": false,
"dhcpd": false,
"pppoe": false
},
"unms": {
"status": 0,
"timestamp": null
},
"wireless": { "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, "antenna_gain": 13,
"frequency": 5500, "apmac": "01:23:45:67:89:AB",
"center1_freq": 5530,
"dfs": 1,
"distance": 0,
"security": "WPA2",
"noisef": -89,
"txpower": -3,
"aprepeater": false, "aprepeater": false,
"rstatus": 5, "band": 2,
"chanbw": 80,
"rx_chainmask": 3,
"tx_chainmask": 3,
"nol_state": 0,
"nol_timeout": 0,
"cac_state": 0, "cac_state": 0,
"cac_timeout": 0, "cac_timeout": 0,
"rx_idx": 8, "center1_freq": 5530,
"rx_nss": 2, "chanbw": 80,
"tx_idx": 9, "compat_11n": 0,
"tx_nss": 2, "count": 1,
"throughput": { "tx": 222, "rx": 9907 }, "dfs": 1,
"service": { "time": 267181, "link": 266003 }, "distance": 0,
"essid": "DemoSSID",
"frequency": 5500,
"hide_essid": 0,
"ieeemode": "11ACVHT80",
"mode": "ap-ptp",
"noisef": -89,
"nol_state": 0,
"nol_timeout": 0,
"polling": { "polling": {
"atpc_status": 2,
"cb_capacity": 593970, "cb_capacity": 593970,
"dl_capacity": 647400, "dl_capacity": 647400,
"ul_capacity": 540540, "ff_cap_rep": false,
"use": 48,
"tx_use": 6,
"rx_use": 42,
"atpc_status": 2,
"fixed_frame": false, "fixed_frame": false,
"flex_mode": null,
"gps_sync": false, "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": [ "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": { "airmax": {
"actual_priority": 0, "actual_priority": 0,
"beam": 0,
"desired_priority": 0,
"cb_capacity": 593970,
"dl_capacity": 647400,
"ul_capacity": 540540,
"atpc_status": 2, "atpc_status": 2,
"beam": 0,
"cb_capacity": 593970,
"desired_priority": 0,
"dl_capacity": 647400,
"rx": { "rx": {
"usage": 42,
"cinr": 31, "cinr": 31,
"evm": [ "evm": [
[ [
@@ -141,10 +203,10 @@
34, 33, 34, 34, 34, 34, 34, 35, 35, 35, 34, 35, 33, 34, 34, 34, 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 34, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35
] ]
] ],
"usage": 42
}, },
"tx": { "tx": {
"usage": 6,
"cinr": 31, "cinr": 31,
"evm": [ "evm": [
[ [
@@ -159,142 +221,127 @@
38, 37, 37, 37, 38, 37, 38, 37, 37, 37, 37, 37, 36, 37, 37, 37, 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 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, "last_disc": 1,
"lastip": "192.168.1.2",
"mac": "01:23:45:67:89:AB",
"noisefloor": -89,
"remote": { "remote": {
"age": 1, "age": 1,
"device_id": "d4f4cdf82961e619328a8f72f8d7653b", "airview": 2,
"hostname": "NanoStation 5AC sta name", "antenna_gain": 13,
"platform": "NanoStation 5AC loco", "cable_loss": 0,
"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,
"chainrssi": [33, 37, 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": [ "tx_ratedata": [
14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154 14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154
], ],
"tx_bytes": 212308148210, "tx_throughput": 16023,
"rx_bytes": 3624206478, "unms": {
"antenna_gain": 13, "status": 0,
"cable_loss": 0, "timestamp": null
"height": 2, },
"ethlist": [ "uptime": 265320,
{ "version": "WA.ar934x.v8.7.17.48152.250620.2132"
"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 }
}, },
"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": [] "sta_disconnected": [],
}, "throughput": {
"interfaces": [ "rx": 9907,
{ "tx": 222
"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
}
}, },
{ "tx_chainmask": 3,
"ifname": "ath0", "tx_idx": 9,
"hwaddr": "01:23:45:67:89:AB", "tx_nss": 2,
"enabled": true, "txpower": -3
"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" }
} }

View File

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

View File

@@ -2,6 +2,7 @@
from copy import deepcopy from copy import deepcopy
import json import json
import logging
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
@@ -395,6 +396,15 @@ async def test_status_with_deprecated_battery_feature(
assert issue.issue_domain == "vacuum" assert issue.issue_domain == "vacuum"
assert issue.translation_key == "deprecated_vacuum_battery_feature" assert issue.translation_key == "deprecated_vacuum_battery_feature"
assert issue.translation_placeholders == {"entity_id": "vacuum.mqtttest"} 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( @pytest.mark.parametrize(

View File

@@ -94,7 +94,7 @@ def mock_config_entry_with_reasoning_model(
hass.config_entries.async_update_subentry( hass.config_entries.async_update_subentry(
mock_config_entry, mock_config_entry,
next(iter(mock_config_entry.subentries.values())), 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 return mock_config_entry

View File

@@ -20,6 +20,7 @@ from homeassistant.components.openai_conversation.const import (
CONF_RECOMMENDED, CONF_RECOMMENDED,
CONF_TEMPERATURE, CONF_TEMPERATURE,
CONF_TOP_P, CONF_TOP_P,
CONF_VERBOSITY,
CONF_WEB_SEARCH, CONF_WEB_SEARCH,
CONF_WEB_SEARCH_CITY, CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_CONTEXT_SIZE, 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_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pirate", CONF_PROMPT: "Speak like a pro",
}, },
{ {
CONF_TEMPERATURE: 1.0, CONF_TEMPERATURE: 1.0,
@@ -317,7 +318,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
), ),
{ {
CONF_RECOMMENDED: False, CONF_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pirate", CONF_PROMPT: "Speak like a pro",
CONF_TEMPERATURE: 1.0, CONF_TEMPERATURE: 1.0,
CONF_CHAT_MODEL: "o1-pro", CONF_CHAT_MODEL: "o1-pro",
CONF_TOP_P: RECOMMENDED_TOP_P, 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 ( # Case 2: reasoning model
{ {
CONF_RECOMMENDED: False, CONF_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pro", CONF_PROMPT: "Speak like a pirate",
CONF_TEMPERATURE: 0.8, CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "o1-pro", CONF_CHAT_MODEL: "gpt-5",
CONF_TOP_P: 0.9, CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000, 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_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pro", CONF_PROMPT: "Speak like a pirate",
}, },
{ {
CONF_TEMPERATURE: 0.8, CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "o1-pro", CONF_CHAT_MODEL: "gpt-5",
CONF_TOP_P: 0.9, CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000, 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_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pro", CONF_PROMPT: "Speak like a pirate",
CONF_TEMPERATURE: 0.8, CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "o1-pro", CONF_CHAT_MODEL: "gpt-5",
CONF_TOP_P: 0.9, CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000, CONF_MAX_TOKENS: 1000,
CONF_REASONING_EFFORT: "high", CONF_REASONING_EFFORT: "minimal",
CONF_CODE_INTERPRETER: False, 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 # 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_PROMPT: "Speak like a pirate",
CONF_LLM_HASS_API: ["assist"], CONF_LLM_HASS_API: ["assist"],
CONF_TEMPERATURE: 0.8, CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "gpt-4o", CONF_CHAT_MODEL: "gpt-5",
CONF_TOP_P: 0.9, CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000, CONF_MAX_TOKENS: 1000,
CONF_REASONING_EFFORT: "high", CONF_REASONING_EFFORT: "high",
CONF_CODE_INTERPRETER: True, 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_PROMPT: "Speak like a pirate",
CONF_LLM_HASS_API: ["assist"], CONF_LLM_HASS_API: ["assist"],
CONF_TEMPERATURE: 0.8, CONF_TEMPERATURE: 0.8,
CONF_CHAT_MODEL: "o3-mini", CONF_CHAT_MODEL: "o5",
CONF_TOP_P: 0.9, CONF_TOP_P: 0.9,
CONF_MAX_TOKENS: 1000, CONF_MAX_TOKENS: 1000,
CONF_REASONING_EFFORT: "low", CONF_REASONING_EFFORT: "low",
CONF_CODE_INTERPRETER: True, CONF_CODE_INTERPRETER: True,
CONF_VERBOSITY: "medium",
}, },
( (
{ {

View File

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

View File

@@ -882,3 +882,23 @@ def ungroup_speakers(coordinator: MockSoCo, group_member: MockSoCo) -> None:
) )
coordinator.zoneGroupTopology.subscribe.return_value._callback(event) coordinator.zoneGroupTopology.subscribe.return_value._callback(event)
group_member.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 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 ( from homeassistant.components.sonos.switch import (
ATTR_DURATION, ATTR_DURATION,
ATTR_ID, ATTR_ID,
ATTR_INCLUDE_LINKED_ZONES, ATTR_INCLUDE_LINKED_ZONES,
ATTR_PLAY_MODE, ATTR_PLAY_MODE,
ATTR_RECURRENCE, ATTR_RECURRENCE,
ATTR_SPEECH_ENHANCEMENT,
ATTR_SPEECH_ENHANCEMENT_ENABLED,
ATTR_VOLUME, ATTR_VOLUME,
) )
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN 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.helpers import entity_registry as er
from homeassistant.util import dt as dt_util 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 from tests.common import async_fire_time_changed
@@ -142,6 +147,49 @@ async def test_switch_attributes(
assert touch_controls_state.state == STATE_ON 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( @pytest.mark.parametrize(
("service", "expected_result"), ("service", "expected_result"),
[ [

View File

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

View File

@@ -1649,6 +1649,58 @@
'state': '220.4', '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] # name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@@ -5,9 +5,10 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
import pytest import pytest
from uiprotect import NotAuthorized, NvrError, ProtectApiClient from uiprotect import NvrError, ProtectApiClient
from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.api import DEVICE_UPDATE_INTERVAL
from uiprotect.data import NVR, Bootstrap, CloudAccount, Light from uiprotect.data import NVR, Bootstrap, CloudAccount, Light
from uiprotect.exceptions import BadRequest, NotAuthorized
from homeassistant.components.unifiprotect.const import ( from homeassistant.components.unifiprotect.const import (
AUTH_RETRIES, AUTH_RETRIES,
@@ -414,6 +415,28 @@ async def test_setup_handles_api_key_creation_failure(
ufp.api.set_api_key.assert_not_called() 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( async def test_setup_with_existing_api_key(
hass: HomeAssistant, ufp: MockUFPFixture hass: HomeAssistant, ufp: MockUFPFixture
) -> None: ) -> None:

View File

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

View File

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

View File

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

View File

@@ -7,8 +7,8 @@
}, },
"electricRange": { "electricRange": {
"status": "OK", "status": "OK",
"value": 220, "value": 150,
"unit": "km", "unit": "mi",
"updatedAt": "2025-07-02T08:51:23Z" "updatedAt": "2025-07-02T08:51:23Z"
}, },
"chargerConnectionStatus": { "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( @pytest.mark.parametrize(
"full_model", "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( async def test_sensor(
hass: HomeAssistant, hass: HomeAssistant,
@@ -30,3 +36,36 @@ async def test_sensor(
assert await setup_integration() assert await setup_integration()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) 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**', 'network_key': '**REDACTED**',
'nwk_addresses': dict({ 'nwk_addresses': dict({
'11:22:33:44:55:66:77:88': 4660,
}), }),
'nwk_manager_id': 0, 'nwk_manager_id': 0,
'nwk_update_id': 0, 'nwk_update_id': 0,

View File

@@ -6,6 +6,7 @@ import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from syrupy.filters import props from syrupy.filters import props
from zigpy.profiles import zha from zigpy.profiles import zha
from zigpy.types import EUI64, NWK
from zigpy.zcl.clusters import security from zigpy.zcl.clusters import security
from homeassistant.components.zha.helpers import ( 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.side_effect = None
gateway.application_controller.energy_scan.return_value = scan 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( diagnostics_data = await get_diagnostics_for_config_entry(
hass, hass_client, config_entry hass, hass_client, config_entry
) )

View File

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

View File

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