mirror of
https://github.com/home-assistant/core.git
synced 2025-08-31 10:21:30 +02:00
2025.8.1 (#150412)
This commit is contained in:
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.2.4"]
|
||||
"requirements": ["airos==0.2.7"]
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"services": {
|
||||
"press": {
|
||||
"name": "Press",
|
||||
"description": "Press the button entity."
|
||||
"description": "Presses a button entity."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.111.1"],
|
||||
"requirements": ["hass-nabucasa==0.111.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""Data update coordinator for the Enigma2 integration."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from openwebif.api import OpenWebIfDevice, OpenWebIfStatus
|
||||
@@ -30,6 +31,8 @@ from .const import CONF_SOURCE_BOUQUET, DOMAIN
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
SETUP_TIMEOUT = 10
|
||||
|
||||
type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator]
|
||||
|
||||
|
||||
@@ -79,7 +82,7 @@ class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]):
|
||||
async def _async_setup(self) -> None:
|
||||
"""Provide needed data to the device info."""
|
||||
|
||||
about = await self.device.get_about()
|
||||
about = await asyncio.wait_for(self.device.get_about(), timeout=SETUP_TIMEOUT)
|
||||
self.device.mac_address = about["info"]["ifaces"][0]["mac"]
|
||||
self.device_info["model"] = about["info"]["model"]
|
||||
self.device_info["manufacturer"] = about["info"]["brand"]
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250806.0"]
|
||||
"requirements": ["home-assistant-frontend==20250811.0"]
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ from aioautomower.exceptions import (
|
||||
ApiError,
|
||||
AuthError,
|
||||
HusqvarnaTimeoutError,
|
||||
HusqvarnaWSClientError,
|
||||
HusqvarnaWSServerHandshakeError,
|
||||
)
|
||||
from aioautomower.model import MowerDictionary
|
||||
@@ -142,7 +143,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
# Reset reconnect time after successful connection
|
||||
self.reconnect_time = DEFAULT_RECONNECT_TIME
|
||||
await automower_client.start_listening()
|
||||
except HusqvarnaWSServerHandshakeError as err:
|
||||
except (HusqvarnaWSServerHandshakeError, HusqvarnaWSClientError) as err:
|
||||
_LOGGER.debug(
|
||||
"Failed to connect to websocket. Trying to reconnect: %s",
|
||||
err,
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["imgw_pib==1.5.2"]
|
||||
"requirements": ["imgw_pib==1.5.3"]
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.8.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.8.6.52906"
|
||||
"knx-frontend==2025.8.9.63154"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Do you want to configure the Launch Library?"
|
||||
"description": "Do you want to configure Launch Library?"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -49,6 +49,7 @@ from .const import (
|
||||
CONF_RECOMMENDED,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TOP_P,
|
||||
CONF_VERBOSITY,
|
||||
CONF_WEB_SEARCH,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE,
|
||||
@@ -67,6 +68,7 @@ from .const import (
|
||||
RECOMMENDED_REASONING_EFFORT,
|
||||
RECOMMENDED_TEMPERATURE,
|
||||
RECOMMENDED_TOP_P,
|
||||
RECOMMENDED_VERBOSITY,
|
||||
RECOMMENDED_WEB_SEARCH,
|
||||
RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE,
|
||||
RECOMMENDED_WEB_SEARCH_USER_LOCATION,
|
||||
@@ -323,7 +325,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
model = options[CONF_CHAT_MODEL]
|
||||
|
||||
if model.startswith("o"):
|
||||
if model.startswith(("o", "gpt-5")):
|
||||
step_schema.update(
|
||||
{
|
||||
vol.Optional(
|
||||
@@ -331,7 +333,9 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
default=RECOMMENDED_REASONING_EFFORT,
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=["low", "medium", "high"],
|
||||
options=["low", "medium", "high"]
|
||||
if model.startswith("o")
|
||||
else ["minimal", "low", "medium", "high"],
|
||||
translation_key=CONF_REASONING_EFFORT,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
@@ -341,6 +345,24 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
elif CONF_REASONING_EFFORT in options:
|
||||
options.pop(CONF_REASONING_EFFORT)
|
||||
|
||||
if model.startswith("gpt-5"):
|
||||
step_schema.update(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_VERBOSITY,
|
||||
default=RECOMMENDED_VERBOSITY,
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=["low", "medium", "high"],
|
||||
translation_key=CONF_VERBOSITY,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
elif CONF_VERBOSITY in options:
|
||||
options.pop(CONF_VERBOSITY)
|
||||
|
||||
if self._subentry_type == "conversation" and not model.startswith(
|
||||
tuple(UNSUPPORTED_WEB_SEARCH_MODELS)
|
||||
):
|
||||
|
@@ -21,6 +21,7 @@ CONF_REASONING_EFFORT = "reasoning_effort"
|
||||
CONF_RECOMMENDED = "recommended"
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
CONF_TOP_P = "top_p"
|
||||
CONF_VERBOSITY = "verbosity"
|
||||
CONF_WEB_SEARCH = "web_search"
|
||||
CONF_WEB_SEARCH_USER_LOCATION = "user_location"
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE = "search_context_size"
|
||||
@@ -34,6 +35,7 @@ RECOMMENDED_MAX_TOKENS = 3000
|
||||
RECOMMENDED_REASONING_EFFORT = "low"
|
||||
RECOMMENDED_TEMPERATURE = 1.0
|
||||
RECOMMENDED_TOP_P = 1.0
|
||||
RECOMMENDED_VERBOSITY = "medium"
|
||||
RECOMMENDED_WEB_SEARCH = False
|
||||
RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE = "medium"
|
||||
RECOMMENDED_WEB_SEARCH_USER_LOCATION = False
|
||||
|
@@ -61,6 +61,7 @@ from .const import (
|
||||
CONF_REASONING_EFFORT,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TOP_P,
|
||||
CONF_VERBOSITY,
|
||||
CONF_WEB_SEARCH,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE,
|
||||
@@ -75,6 +76,7 @@ from .const import (
|
||||
RECOMMENDED_REASONING_EFFORT,
|
||||
RECOMMENDED_TEMPERATURE,
|
||||
RECOMMENDED_TOP_P,
|
||||
RECOMMENDED_VERBOSITY,
|
||||
RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE,
|
||||
)
|
||||
|
||||
@@ -346,14 +348,18 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
if tools:
|
||||
model_args["tools"] = tools
|
||||
|
||||
if model_args["model"].startswith("o"):
|
||||
if model_args["model"].startswith(("o", "gpt-5")):
|
||||
model_args["reasoning"] = {
|
||||
"effort": options.get(
|
||||
CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT
|
||||
)
|
||||
}
|
||||
else:
|
||||
model_args["store"] = False
|
||||
model_args["include"] = ["reasoning.encrypted_content"]
|
||||
|
||||
if model_args["model"].startswith("gpt-5"):
|
||||
model_args["text"] = {
|
||||
"verbosity": options.get(CONF_VERBOSITY, RECOMMENDED_VERBOSITY)
|
||||
}
|
||||
|
||||
messages = [
|
||||
m
|
||||
|
@@ -121,6 +121,7 @@
|
||||
"selector": {
|
||||
"reasoning_effort": {
|
||||
"options": {
|
||||
"minimal": "Minimal",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"high": "[%key:common::state::high%]"
|
||||
@@ -132,6 +133,13 @@
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"high": "[%key:common::state::high%]"
|
||||
}
|
||||
},
|
||||
"verbosity": {
|
||||
"options": {
|
||||
"low": "[%key:common::state::low%]",
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"high": "[%key:common::state::high%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@@ -30,9 +30,9 @@ async def validate_input(hass: HomeAssistant, data):
|
||||
|
||||
return {
|
||||
"title": is_valid["title"],
|
||||
"relay_count": is_valid["relay_count"],
|
||||
"input_count": is_valid["input_count"],
|
||||
"is_old": is_valid["is_old"],
|
||||
"relay_count": is_valid["relays"],
|
||||
"input_count": is_valid["inputs"],
|
||||
"is_old": is_valid["temps"],
|
||||
}
|
||||
|
||||
|
||||
|
@@ -186,6 +186,9 @@ MODELS_TV_ONLY = (
|
||||
"ULTRA",
|
||||
)
|
||||
MODELS_LINEIN_AND_TV = ("AMP",)
|
||||
MODEL_SONOS_ARC_ULTRA = "SONOS ARC ULTRA"
|
||||
|
||||
ATTR_SPEECH_ENHANCEMENT_ENABLED = "speech_enhance_enabled"
|
||||
|
||||
AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1)
|
||||
AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5
|
||||
|
@@ -35,6 +35,7 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from .alarms import SonosAlarms
|
||||
from .const import (
|
||||
ATTR_SPEECH_ENHANCEMENT_ENABLED,
|
||||
AVAILABILITY_TIMEOUT,
|
||||
BATTERY_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
@@ -157,6 +158,7 @@ class SonosSpeaker:
|
||||
# Home theater
|
||||
self.audio_delay: int | None = None
|
||||
self.dialog_level: bool | None = None
|
||||
self.speech_enhance_enabled: bool | None = None
|
||||
self.night_mode: bool | None = None
|
||||
self.sub_enabled: bool | None = None
|
||||
self.sub_crossover: int | None = None
|
||||
@@ -548,6 +550,11 @@ class SonosSpeaker:
|
||||
@callback
|
||||
def async_update_volume(self, event: SonosEvent) -> None:
|
||||
"""Update information about currently volume settings."""
|
||||
_LOGGER.debug(
|
||||
"Updating volume for %s with event variables: %s",
|
||||
self.zone_name,
|
||||
event.variables,
|
||||
)
|
||||
self.event_stats.process(event)
|
||||
variables = event.variables
|
||||
|
||||
@@ -565,6 +572,7 @@ class SonosSpeaker:
|
||||
|
||||
for bool_var in (
|
||||
"dialog_level",
|
||||
ATTR_SPEECH_ENHANCEMENT_ENABLED,
|
||||
"night_mode",
|
||||
"sub_enabled",
|
||||
"surround_enabled",
|
||||
|
@@ -19,7 +19,9 @@ from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
from .alarms import SonosAlarms
|
||||
from .const import (
|
||||
ATTR_SPEECH_ENHANCEMENT_ENABLED,
|
||||
DOMAIN,
|
||||
MODEL_SONOS_ARC_ULTRA,
|
||||
SONOS_ALARMS_UPDATED,
|
||||
SONOS_CREATE_ALARM,
|
||||
SONOS_CREATE_SWITCHES,
|
||||
@@ -59,6 +61,7 @@ ALL_FEATURES = (
|
||||
ATTR_SURROUND_ENABLED,
|
||||
ATTR_STATUS_LIGHT,
|
||||
)
|
||||
ALL_SUBST_FEATURES = (ATTR_SPEECH_ENHANCEMENT_ENABLED,)
|
||||
|
||||
COORDINATOR_FEATURES = ATTR_CROSSFADE
|
||||
|
||||
@@ -69,6 +72,14 @@ POLL_REQUIRED = (
|
||||
|
||||
WEEKEND_DAYS = (0, 6)
|
||||
|
||||
# Mapping of model names to feature attributes that need to be substituted.
|
||||
# This is used to handle differences in attributes across Sonos models.
|
||||
MODEL_FEATURE_SUBSTITUTIONS: dict[str, dict[str, str]] = {
|
||||
MODEL_SONOS_ARC_ULTRA: {
|
||||
ATTR_SPEECH_ENHANCEMENT: ATTR_SPEECH_ENHANCEMENT_ENABLED,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -92,6 +103,13 @@ async def async_setup_entry(
|
||||
|
||||
def available_soco_attributes(speaker: SonosSpeaker) -> list[str]:
|
||||
features = []
|
||||
for feature_type in ALL_SUBST_FEATURES:
|
||||
try:
|
||||
if (state := getattr(speaker.soco, feature_type, None)) is not None:
|
||||
setattr(speaker, feature_type, state)
|
||||
except SoCoSlaveException:
|
||||
pass
|
||||
|
||||
for feature_type in ALL_FEATURES:
|
||||
try:
|
||||
if (state := getattr(speaker.soco, feature_type, None)) is not None:
|
||||
@@ -107,12 +125,23 @@ async def async_setup_entry(
|
||||
available_soco_attributes, speaker
|
||||
)
|
||||
for feature_type in available_features:
|
||||
attribute_key = MODEL_FEATURE_SUBSTITUTIONS.get(
|
||||
speaker.model_name.upper(), {}
|
||||
).get(feature_type, feature_type)
|
||||
_LOGGER.debug(
|
||||
"Creating %s switch on %s",
|
||||
"Creating %s switch on %s attribute %s",
|
||||
feature_type,
|
||||
speaker.zone_name,
|
||||
attribute_key,
|
||||
)
|
||||
entities.append(
|
||||
SonosSwitchEntity(
|
||||
feature_type=feature_type,
|
||||
attribute_key=attribute_key,
|
||||
speaker=speaker,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
)
|
||||
entities.append(SonosSwitchEntity(feature_type, speaker, config_entry))
|
||||
async_add_entities(entities)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
@@ -127,11 +156,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
|
||||
"""Representation of a Sonos feature switch."""
|
||||
|
||||
def __init__(
|
||||
self, feature_type: str, speaker: SonosSpeaker, config_entry: SonosConfigEntry
|
||||
self,
|
||||
feature_type: str,
|
||||
attribute_key: str,
|
||||
speaker: SonosSpeaker,
|
||||
config_entry: SonosConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(speaker, config_entry)
|
||||
self.feature_type = feature_type
|
||||
self.attribute_key = attribute_key
|
||||
self.needs_coordinator = feature_type in COORDINATOR_FEATURES
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
self._attr_translation_key = feature_type
|
||||
@@ -149,15 +182,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
|
||||
@soco_error()
|
||||
def poll_state(self) -> None:
|
||||
"""Poll the current state of the switch."""
|
||||
state = getattr(self.soco, self.feature_type)
|
||||
setattr(self.speaker, self.feature_type, state)
|
||||
state = getattr(self.soco, self.attribute_key)
|
||||
setattr(self.speaker, self.attribute_key, state)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
if self.needs_coordinator and not self.speaker.is_coordinator:
|
||||
return cast(bool, getattr(self.speaker.coordinator, self.feature_type))
|
||||
return cast(bool, getattr(self.speaker, self.feature_type))
|
||||
return cast(bool, getattr(self.speaker.coordinator, self.attribute_key))
|
||||
return cast(bool, getattr(self.speaker, self.attribute_key))
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
@@ -175,7 +208,7 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
|
||||
else:
|
||||
soco = self.soco
|
||||
try:
|
||||
setattr(soco, self.feature_type, enable)
|
||||
setattr(soco, self.attribute_key, enable)
|
||||
except SoCoUPnPException as exc:
|
||||
_LOGGER.warning("Could not toggle %s: %s", self.entity_id, exc)
|
||||
|
||||
|
@@ -157,26 +157,28 @@ class BrowseData:
|
||||
|
||||
cmd = ["apps", 0, browse_limit]
|
||||
result = await player.async_query(*cmd)
|
||||
for app in result["appss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
self.add_new_command(app_cmd, "item_id")
|
||||
_LOGGER.debug(
|
||||
"Adding new command %s to browse data for player %s",
|
||||
app_cmd,
|
||||
player.player_id,
|
||||
)
|
||||
if result["appss_loop"]:
|
||||
for app in result["appss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
self.add_new_command(app_cmd, "item_id")
|
||||
_LOGGER.debug(
|
||||
"Adding new command %s to browse data for player %s",
|
||||
app_cmd,
|
||||
player.player_id,
|
||||
)
|
||||
cmd = ["radios", 0, browse_limit]
|
||||
result = await player.async_query(*cmd)
|
||||
for app in result["radioss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
self.add_new_command(app_cmd, "item_id")
|
||||
_LOGGER.debug(
|
||||
"Adding new command %s to browse data for player %s",
|
||||
app_cmd,
|
||||
player.player_id,
|
||||
)
|
||||
if result["radioss_loop"]:
|
||||
for app in result["radioss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
self.add_new_command(app_cmd, "item_id")
|
||||
_LOGGER.debug(
|
||||
"Adding new command %s to browse data for player %s",
|
||||
app_cmd,
|
||||
player.player_id,
|
||||
)
|
||||
|
||||
|
||||
def _build_response_apps_radios_category(
|
||||
|
@@ -325,7 +325,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def volume_level(self) -> float | None:
|
||||
"""Volume level of the media player (0..1)."""
|
||||
if self._player.volume:
|
||||
if self._player.volume is not None:
|
||||
return int(float(self._player.volume)) / 100.0
|
||||
|
||||
return None
|
||||
|
@@ -299,7 +299,10 @@ async def async_setup_entry(
|
||||
)
|
||||
await home.rt_subscribe(
|
||||
TibberRtDataCoordinator(
|
||||
entity_creator.add_sensors, home, hass
|
||||
hass,
|
||||
entry,
|
||||
entity_creator.add_sensors,
|
||||
home,
|
||||
).async_set_updated_data
|
||||
)
|
||||
|
||||
@@ -613,15 +616,17 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
add_sensor_callback: Callable[[TibberRtDataCoordinator, Any], None],
|
||||
tibber_home: tibber.TibberHome,
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Initialize the data handler."""
|
||||
self._add_sensor_callback = add_sensor_callback
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=tibber_home.info["viewer"]["home"]["address"].get(
|
||||
"address1", "Tibber"
|
||||
),
|
||||
|
@@ -99,8 +99,22 @@ class EnumTypeData:
|
||||
return cls(dpcode, **parsed)
|
||||
|
||||
|
||||
class ComplexTypeData:
|
||||
"""Complex Type Data (for JSON/RAW parsing)."""
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: str) -> Self:
|
||||
"""Load JSON string and return a ComplexTypeData object."""
|
||||
raise NotImplementedError("from_json is not implemented for this type")
|
||||
|
||||
@classmethod
|
||||
def from_raw(cls, data: str) -> Self | None:
|
||||
"""Decode base64 string and return a ComplexTypeData object."""
|
||||
raise NotImplementedError("from_raw is not implemented for this type")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ElectricityTypeData:
|
||||
class ElectricityTypeData(ComplexTypeData):
|
||||
"""Electricity Type Data."""
|
||||
|
||||
electriccurrent: str | None = None
|
||||
@@ -113,9 +127,11 @@ class ElectricityTypeData:
|
||||
return cls(**json.loads(data.lower()))
|
||||
|
||||
@classmethod
|
||||
def from_raw(cls, data: str) -> Self:
|
||||
def from_raw(cls, data: str) -> Self | None:
|
||||
"""Decode base64 string and return a ElectricityTypeData object."""
|
||||
raw = base64.b64decode(data)
|
||||
if len(raw) == 0:
|
||||
return None
|
||||
voltage = struct.unpack(">H", raw[0:2])[0] / 10.0
|
||||
electriccurrent = struct.unpack(">L", b"\x00" + raw[2:5])[0] / 1000.0
|
||||
power = struct.unpack(">L", b"\x00" + raw[5:8])[0] / 1000.0
|
||||
|
@@ -40,13 +40,14 @@ from .const import (
|
||||
UnitOfMeasurement,
|
||||
)
|
||||
from .entity import TuyaEntity
|
||||
from .models import ElectricityTypeData, EnumTypeData, IntegerTypeData
|
||||
from .models import ComplexTypeData, ElectricityTypeData, EnumTypeData, IntegerTypeData
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TuyaSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Tuya sensor entity."""
|
||||
|
||||
complex_type: type[ComplexTypeData] | None = None
|
||||
subkey: str | None = None
|
||||
|
||||
|
||||
@@ -368,6 +369,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="electriccurrent",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -376,6 +378,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="power",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -384,6 +387,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="voltage",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -392,6 +396,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="electriccurrent",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -400,6 +405,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="power",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -408,6 +414,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="voltage",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -416,6 +423,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="electriccurrent",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -424,6 +432,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="power",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -432,6 +441,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="voltage",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1254,6 +1264,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
translation_key="total_power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="power",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1262,6 +1273,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="electriccurrent",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1270,6 +1282,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="power",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1278,6 +1291,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="voltage",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1286,6 +1300,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="electriccurrent",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1294,6 +1309,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="power",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1302,6 +1318,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="voltage",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1310,6 +1327,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="electriccurrent",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1318,6 +1336,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="power",
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
@@ -1326,6 +1345,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
complex_type=ElectricityTypeData,
|
||||
subkey="voltage",
|
||||
),
|
||||
),
|
||||
@@ -1424,7 +1444,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||
|
||||
_status_range: DeviceStatusRange | None = None
|
||||
_type: DPType | None = None
|
||||
_type_data: IntegerTypeData | EnumTypeData | None = None
|
||||
_type_data: IntegerTypeData | EnumTypeData | ComplexTypeData | None = None
|
||||
_uom: UnitOfMeasurement | None = None
|
||||
|
||||
def __init__(
|
||||
@@ -1476,6 +1496,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||
self.unique_id,
|
||||
)
|
||||
self._attr_device_class = None
|
||||
self._attr_suggested_unit_of_measurement = None
|
||||
return
|
||||
|
||||
uoms = DEVICE_CLASS_UNITS[self.device_class]
|
||||
@@ -1486,6 +1507,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||
# Unknown unit of measurement, device class should not be used.
|
||||
if uom is None:
|
||||
self._attr_device_class = None
|
||||
self._attr_suggested_unit_of_measurement = None
|
||||
return
|
||||
|
||||
# Found unit of measurement, use the standardized Unit
|
||||
@@ -1523,16 +1545,23 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||
|
||||
# Get subkey value from Json string.
|
||||
if self._type is DPType.JSON:
|
||||
if self.entity_description.subkey is None:
|
||||
if (
|
||||
self.entity_description.complex_type is None
|
||||
or self.entity_description.subkey is None
|
||||
):
|
||||
return None
|
||||
values = ElectricityTypeData.from_json(value)
|
||||
values = self.entity_description.complex_type.from_json(value)
|
||||
return getattr(values, self.entity_description.subkey)
|
||||
|
||||
if self._type is DPType.RAW:
|
||||
if self.entity_description.subkey is None:
|
||||
if (
|
||||
self.entity_description.complex_type is None
|
||||
or self.entity_description.subkey is None
|
||||
or (raw_values := self.entity_description.complex_type.from_raw(value))
|
||||
is None
|
||||
):
|
||||
return None
|
||||
values = ElectricityTypeData.from_raw(value)
|
||||
return getattr(values, self.entity_description.subkey)
|
||||
return getattr(raw_values, self.entity_description.subkey)
|
||||
|
||||
# Valid string or enum value
|
||||
return value
|
||||
|
@@ -8,7 +8,7 @@ import logging
|
||||
from aiohttp.client_exceptions import ServerDisconnectedError
|
||||
from uiprotect.api import DEVICE_UPDATE_INTERVAL
|
||||
from uiprotect.data import Bootstrap
|
||||
from uiprotect.exceptions import ClientError, NotAuthorized
|
||||
from uiprotect.exceptions import BadRequest, ClientError, NotAuthorized
|
||||
|
||||
# Import the test_util.anonymize module from the uiprotect package
|
||||
# in __init__ to ensure it gets imported in the executor since the
|
||||
@@ -100,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
|
||||
new_api_key = await protect.create_api_key(
|
||||
name=f"Home Assistant ({hass.config.location_name})"
|
||||
)
|
||||
except NotAuthorized as err:
|
||||
except (NotAuthorized, BadRequest) as err:
|
||||
_LOGGER.error("Failed to create API key: %s", err)
|
||||
else:
|
||||
protect.set_api_key(new_api_key)
|
||||
|
@@ -104,7 +104,12 @@ def async_migrate_entities_unique_ids(
|
||||
f"{registry_entry.config_entry_id}_"
|
||||
).removesuffix(f"_{registry_entry.translation_key}")
|
||||
if monitor := next(
|
||||
(m for m in metrics.values() if m.monitor_name == name), None
|
||||
(
|
||||
m
|
||||
for m in metrics.values()
|
||||
if m.monitor_name == name and m.monitor_id is not None
|
||||
),
|
||||
None,
|
||||
):
|
||||
entity_registry.async_update_entity(
|
||||
registry_entry.entity_id,
|
||||
|
@@ -79,7 +79,10 @@ DEFAULT_NAME = "Vacuum cleaner robot"
|
||||
_DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2026.1")
|
||||
_DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1")
|
||||
|
||||
_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",)
|
||||
_BATTERY_DEPRECATION_IGNORED_PLATFORMS = (
|
||||
"mqtt",
|
||||
"template",
|
||||
)
|
||||
|
||||
|
||||
class VacuumEntityFeature(IntFlag):
|
||||
@@ -333,7 +336,7 @@ class StateVacuumEntity(
|
||||
f"is setting the {property} which has been deprecated."
|
||||
f" Integration {self.platform.platform_name} should implement a sensor"
|
||||
" instead with a correct device class and link it to the same device",
|
||||
core_integration_behavior=ReportBehavior.LOG,
|
||||
core_integration_behavior=ReportBehavior.IGNORE,
|
||||
custom_integration_behavior=ReportBehavior.LOG,
|
||||
breaks_in_ha_version="2026.8",
|
||||
integration_domain=self.platform.platform_name,
|
||||
@@ -358,7 +361,7 @@ class StateVacuumEntity(
|
||||
f" Integration {self.platform.platform_name} should remove this as part of migrating"
|
||||
" the battery level and icon to a sensor",
|
||||
core_behavior=ReportBehavior.LOG,
|
||||
core_integration_behavior=ReportBehavior.LOG,
|
||||
core_integration_behavior=ReportBehavior.IGNORE,
|
||||
custom_integration_behavior=ReportBehavior.LOG,
|
||||
breaks_in_ha_version="2026.8",
|
||||
integration_domain=self.platform.platform_name,
|
||||
|
@@ -15,6 +15,7 @@ from volvocarsapi.models import (
|
||||
VolvoAuthException,
|
||||
VolvoCarsApiBaseModel,
|
||||
VolvoCarsValue,
|
||||
VolvoCarsValueStatusField,
|
||||
VolvoCarsVehicle,
|
||||
)
|
||||
|
||||
@@ -36,6 +37,16 @@ type VolvoConfigEntry = ConfigEntry[tuple[VolvoBaseCoordinator, ...]]
|
||||
type CoordinatorData = dict[str, VolvoCarsApiBaseModel | None]
|
||||
|
||||
|
||||
def _is_invalid_api_field(field: VolvoCarsApiBaseModel | None) -> bool:
|
||||
if not field:
|
||||
return True
|
||||
|
||||
if isinstance(field, VolvoCarsValueStatusField) and field.status == "ERROR":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
||||
"""Volvo base coordinator."""
|
||||
|
||||
@@ -121,7 +132,13 @@ class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
||||
translation_key="update_failed",
|
||||
) from result
|
||||
|
||||
data |= cast(CoordinatorData, result)
|
||||
api_data = cast(CoordinatorData, result)
|
||||
data |= {
|
||||
key: field
|
||||
for key, field in api_data.items()
|
||||
if not _is_invalid_api_field(field)
|
||||
}
|
||||
|
||||
valid = True
|
||||
|
||||
# Raise an error if not a single API call succeeded
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, replace
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -47,7 +47,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription):
|
||||
"""Describes a Volvo sensor entity."""
|
||||
|
||||
source_fields: list[str] | None = None
|
||||
value_fn: Callable[[VolvoCarsValue], Any] | None = None
|
||||
|
||||
|
||||
@@ -87,7 +86,12 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
_CHARGING_POWER_STATUS_OPTIONS = ["providing_power", "no_power_available"]
|
||||
_CHARGING_POWER_STATUS_OPTIONS = [
|
||||
"fault",
|
||||
"power_available_but_not_activated",
|
||||
"providing_power",
|
||||
"no_power_available",
|
||||
]
|
||||
|
||||
_DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
|
||||
# command-accessibility endpoint
|
||||
@@ -110,6 +114,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
|
||||
api_field="averageEnergyConsumption",
|
||||
native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
# statistics endpoint
|
||||
VolvoSensorDescription(
|
||||
@@ -117,6 +122,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
|
||||
api_field="averageEnergyConsumptionAutomatic",
|
||||
native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
# statistics endpoint
|
||||
VolvoSensorDescription(
|
||||
@@ -124,6 +130,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
|
||||
api_field="averageEnergyConsumptionSinceCharge",
|
||||
native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
# statistics endpoint
|
||||
VolvoSensorDescription(
|
||||
@@ -131,6 +138,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
|
||||
api_field="averageFuelConsumption",
|
||||
native_unit_of_measurement="L/100 km",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
# statistics endpoint
|
||||
VolvoSensorDescription(
|
||||
@@ -138,6 +146,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
|
||||
api_field="averageFuelConsumptionAutomatic",
|
||||
native_unit_of_measurement="L/100 km",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
# statistics endpoint
|
||||
VolvoSensorDescription(
|
||||
@@ -235,11 +244,15 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
|
||||
"none",
|
||||
],
|
||||
),
|
||||
# statistics & energy state endpoint
|
||||
# statistics endpoint
|
||||
# We're not using `electricRange` from the energy state endpoint because
|
||||
# the official app seems to use `distanceToEmptyBattery`.
|
||||
# In issue #150213, a user described to behavior as follows:
|
||||
# - For a `distanceToEmptyBattery` of 250km, the `electricRange` was 150mi
|
||||
# - For a `distanceToEmptyBattery` of 260km, the `electricRange` was 160mi
|
||||
VolvoSensorDescription(
|
||||
key="distance_to_empty_battery",
|
||||
api_field="",
|
||||
source_fields=["distanceToEmptyBattery", "electricRange"],
|
||||
api_field="distanceToEmptyBattery",
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -357,12 +370,7 @@ async def async_setup_entry(
|
||||
if description.key in added_keys:
|
||||
continue
|
||||
|
||||
if description.source_fields:
|
||||
for field in description.source_fields:
|
||||
if field in coordinator.data:
|
||||
description = replace(description, api_field=field)
|
||||
_add_entity(coordinator, description)
|
||||
elif description.api_field in coordinator.data:
|
||||
if description.api_field in coordinator.data:
|
||||
_add_entity(coordinator, description)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
@@ -94,7 +94,7 @@
|
||||
"state": {
|
||||
"connected": "[%key:common::state::connected%]",
|
||||
"disconnected": "[%key:common::state::disconnected%]",
|
||||
"fault": "[%key:common::state::error%]"
|
||||
"fault": "[%key:common::state::fault%]"
|
||||
}
|
||||
},
|
||||
"charging_current_limit": {
|
||||
@@ -106,6 +106,8 @@
|
||||
"charging_power_status": {
|
||||
"name": "Charging power status",
|
||||
"state": {
|
||||
"fault": "[%key:common::state::fault%]",
|
||||
"power_available_but_not_activated": "Power available",
|
||||
"providing_power": "Providing power",
|
||||
"no_power_available": "No power"
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ from typing import Any
|
||||
|
||||
from zha.application.const import ATTR_IEEE
|
||||
from zha.application.gateway import Gateway
|
||||
from zigpy.application import ControllerApplication
|
||||
from zigpy.config import CONF_NWK_EXTENDED_PAN_ID
|
||||
from zigpy.types import Channels
|
||||
|
||||
@@ -63,6 +64,19 @@ def shallow_asdict(obj: Any) -> dict:
|
||||
return obj
|
||||
|
||||
|
||||
def get_application_state_diagnostics(app: ControllerApplication) -> dict:
|
||||
"""Dump the application state as a dictionary."""
|
||||
data = shallow_asdict(app.state)
|
||||
|
||||
# EUI64 objects in zigpy are not subclasses of any JSON-serializable key type and
|
||||
# must be converted to strings.
|
||||
data["network_info"]["nwk_addresses"] = {
|
||||
str(k): v for k, v in data["network_info"]["nwk_addresses"].items()
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
@@ -79,7 +93,7 @@ async def async_get_config_entry_diagnostics(
|
||||
{
|
||||
"config": zha_data.yaml_config,
|
||||
"config_entry": config_entry.as_dict(),
|
||||
"application_state": shallow_asdict(app.state),
|
||||
"application_state": get_application_state_diagnostics(app),
|
||||
"energy_scan": {
|
||||
channel: 100 * energy / 255 for channel, energy in energy_scan.items()
|
||||
},
|
||||
|
@@ -21,7 +21,7 @@
|
||||
"zha",
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": ["zha==0.0.66"],
|
||||
"requirements": ["zha==0.0.68"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10C4",
|
||||
|
@@ -43,7 +43,7 @@ from .models import ZwaveJSConfigEntry
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
UPDATE_DELAY_STRING = "delay"
|
||||
UPDATE_DELAY_INTERVAL = 5 # In minutes
|
||||
UPDATE_DELAY_INTERVAL = 15 # In seconds
|
||||
ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware"
|
||||
|
||||
|
||||
@@ -130,11 +130,11 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def async_add_firmware_update_entity(node: ZwaveNode) -> None:
|
||||
"""Add firmware update entity."""
|
||||
# We need to delay the first update of each entity to avoid flooding the network
|
||||
# so we maintain a counter to schedule first update in UPDATE_DELAY_INTERVAL
|
||||
# minute increments.
|
||||
# Delay the first update of each entity to avoid spamming the firmware server.
|
||||
# Maintain a counter to schedule first update in UPDATE_DELAY_INTERVAL
|
||||
# second increments.
|
||||
cnt[UPDATE_DELAY_STRING] += 1
|
||||
delay = timedelta(minutes=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL))
|
||||
delay = timedelta(seconds=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL))
|
||||
driver = client.driver
|
||||
assert driver is not None # Driver is ready before platforms are loaded.
|
||||
if node.is_controller_node:
|
||||
@@ -429,7 +429,8 @@ class ZWaveFirmwareUpdateEntity(UpdateEntity):
|
||||
):
|
||||
self._attr_latest_version = self._attr_installed_version
|
||||
|
||||
# Spread updates out in 5 minute increments to avoid flooding the network
|
||||
# Spread updates out in 15 second increments
|
||||
# to avoid spamming the firmware server
|
||||
self.async_on_remove(
|
||||
async_call_later(self.hass, self._delay, self._async_update)
|
||||
)
|
||||
|
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 8
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "1"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
|
@@ -35,10 +35,10 @@ fnv-hash-fast==1.5.0
|
||||
go2rtc-client==0.2.1
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==4.0.2
|
||||
hass-nabucasa==0.111.1
|
||||
hass-nabucasa==0.111.2
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250806.0
|
||||
home-assistant-frontend==20250811.0
|
||||
home-assistant-intents==2025.7.30
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
@@ -213,3 +213,6 @@ multidict>=6.4.2
|
||||
# Stable Alpine current only ships cargo 1.83.0
|
||||
# No wheels upstream available for armhf & armv7
|
||||
rpds-py==0.24.0
|
||||
|
||||
# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI
|
||||
num2words==0.5.14
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.8.0"
|
||||
version = "2025.8.1"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -47,7 +47,7 @@ dependencies = [
|
||||
"fnv-hash-fast==1.5.0",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==0.111.1",
|
||||
"hass-nabucasa==0.111.2",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.28.1",
|
||||
|
2
requirements.txt
generated
2
requirements.txt
generated
@@ -22,7 +22,7 @@ certifi>=2021.5.30
|
||||
ciso8601==2.3.2
|
||||
cronsim==2.6
|
||||
fnv-hash-fast==1.5.0
|
||||
hass-nabucasa==0.111.1
|
||||
hass-nabucasa==0.111.2
|
||||
httpx==0.28.1
|
||||
home-assistant-bluetooth==1.13.1
|
||||
ifaddr==0.2.0
|
||||
|
12
requirements_all.txt
generated
12
requirements_all.txt
generated
@@ -453,7 +453,7 @@ airgradient==0.9.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airos
|
||||
airos==0.2.4
|
||||
airos==0.2.7
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.9.2
|
||||
@@ -1133,7 +1133,7 @@ habiticalib==0.4.1
|
||||
habluetooth==4.0.2
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.111.1
|
||||
hass-nabucasa==0.111.2
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass-splunk==0.1.1
|
||||
@@ -1174,7 +1174,7 @@ hole==0.9.0
|
||||
holidays==0.78
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250806.0
|
||||
home-assistant-frontend==20250811.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.7.30
|
||||
@@ -1240,7 +1240,7 @@ ihcsdk==2.8.5
|
||||
imeon_inverter_api==0.3.14
|
||||
|
||||
# homeassistant.components.imgw_pib
|
||||
imgw_pib==1.5.2
|
||||
imgw_pib==1.5.3
|
||||
|
||||
# homeassistant.components.incomfort
|
||||
incomfort-client==0.6.9
|
||||
@@ -1307,7 +1307,7 @@ kiwiki-client==0.1.1
|
||||
knocki==0.4.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2025.8.6.52906
|
||||
knx-frontend==2025.8.9.63154
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
@@ -3203,7 +3203,7 @@ zeroconf==0.147.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.66
|
||||
zha==0.0.68
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.13
|
||||
|
12
requirements_test_all.txt
generated
12
requirements_test_all.txt
generated
@@ -435,7 +435,7 @@ airgradient==0.9.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airos
|
||||
airos==0.2.4
|
||||
airos==0.2.7
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.9.2
|
||||
@@ -994,7 +994,7 @@ habiticalib==0.4.1
|
||||
habluetooth==4.0.2
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.111.1
|
||||
hass-nabucasa==0.111.2
|
||||
|
||||
# homeassistant.components.assist_satellite
|
||||
# homeassistant.components.conversation
|
||||
@@ -1023,7 +1023,7 @@ hole==0.9.0
|
||||
holidays==0.78
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250806.0
|
||||
home-assistant-frontend==20250811.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.7.30
|
||||
@@ -1074,7 +1074,7 @@ igloohome-api==0.1.1
|
||||
imeon_inverter_api==0.3.14
|
||||
|
||||
# homeassistant.components.imgw_pib
|
||||
imgw_pib==1.5.2
|
||||
imgw_pib==1.5.3
|
||||
|
||||
# homeassistant.components.incomfort
|
||||
incomfort-client==0.6.9
|
||||
@@ -1129,7 +1129,7 @@ kegtron-ble==0.4.0
|
||||
knocki==0.4.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2025.8.6.52906
|
||||
knx-frontend==2025.8.9.63154
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
@@ -2647,7 +2647,7 @@ zeroconf==0.147.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.66
|
||||
zha==0.0.68
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.67.1
|
||||
|
@@ -239,6 +239,9 @@ multidict>=6.4.2
|
||||
# Stable Alpine current only ships cargo 1.83.0
|
||||
# No wheels upstream available for armhf & armv7
|
||||
rpds-py==0.24.0
|
||||
|
||||
# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI
|
||||
num2words==0.5.14
|
||||
"""
|
||||
|
||||
GENERATED_MESSAGE = (
|
||||
|
@@ -15,7 +15,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
@pytest.fixture
|
||||
def ap_fixture():
|
||||
"""Load fixture data for AP mode."""
|
||||
json_data = load_json_object_fixture("airos_ap-ptp.json", DOMAIN)
|
||||
json_data = load_json_object_fixture("airos_loco5ac_ap-ptp.json", DOMAIN)
|
||||
return AirOSData.from_dict(json_data)
|
||||
|
||||
|
||||
|
@@ -1,132 +1,194 @@
|
||||
{
|
||||
"chain_names": [
|
||||
{ "number": 1, "name": "Chain 0" },
|
||||
{ "number": 2, "name": "Chain 1" }
|
||||
{
|
||||
"name": "Chain 0",
|
||||
"number": 1
|
||||
},
|
||||
{
|
||||
"name": "Chain 1",
|
||||
"number": 2
|
||||
}
|
||||
],
|
||||
"host": {
|
||||
"hostname": "NanoStation 5AC ap name",
|
||||
"device_id": "03aa0d0b40fed0a47088293584ef5432",
|
||||
"uptime": 264888,
|
||||
"power_time": 268683,
|
||||
"time": "2025-06-23 23:06:42",
|
||||
"timestamp": 2668313184,
|
||||
"fwversion": "v8.7.17",
|
||||
"devmodel": "NanoStation 5AC loco",
|
||||
"netrole": "bridge",
|
||||
"loadavg": 0.412598,
|
||||
"totalram": 63447040,
|
||||
"freeram": 16564224,
|
||||
"temperature": 0,
|
||||
"cpuload": 10.10101,
|
||||
"height": 3
|
||||
},
|
||||
"genuine": "/images/genuine.png",
|
||||
"services": {
|
||||
"dhcpc": false,
|
||||
"dhcpd": false,
|
||||
"dhcp6d_stateful": false,
|
||||
"pppoe": false,
|
||||
"airview": 2
|
||||
"derived": {
|
||||
"access_point": true,
|
||||
"mac": "01:23:45:67:89:AB",
|
||||
"mac_interface": "br0",
|
||||
"ptmp": false,
|
||||
"ptp": true,
|
||||
"station": false
|
||||
},
|
||||
"firewall": {
|
||||
"iptables": false,
|
||||
"eb6tables": false,
|
||||
"ebtables": false,
|
||||
"ip6tables": false,
|
||||
"eb6tables": false
|
||||
"iptables": false
|
||||
},
|
||||
"genuine": "/images/genuine.png",
|
||||
"gps": {
|
||||
"fix": 0,
|
||||
"lat": 52.379894,
|
||||
"lon": 4.901608
|
||||
},
|
||||
"host": {
|
||||
"cpuload": 10.10101,
|
||||
"device_id": "03aa0d0b40fed0a47088293584ef5432",
|
||||
"devmodel": "NanoStation 5AC loco",
|
||||
"freeram": 16564224,
|
||||
"fwversion": "v8.7.17",
|
||||
"height": 3,
|
||||
"hostname": "NanoStation 5AC ap name",
|
||||
"loadavg": 0.412598,
|
||||
"netrole": "bridge",
|
||||
"power_time": 268683,
|
||||
"temperature": 0,
|
||||
"time": "2025-06-23 23:06:42",
|
||||
"timestamp": 2668313184,
|
||||
"totalram": 63447040,
|
||||
"uptime": 264888
|
||||
},
|
||||
"interfaces": [
|
||||
{
|
||||
"enabled": true,
|
||||
"hwaddr": "01:23:45:67:89:AB",
|
||||
"ifname": "eth0",
|
||||
"mtu": 1500,
|
||||
"status": {
|
||||
"cable_len": 18,
|
||||
"duplex": true,
|
||||
"ip6addr": null,
|
||||
"ipaddr": "0.0.0.0",
|
||||
"plugged": true,
|
||||
"rx_bytes": 3984971949,
|
||||
"rx_dropped": 0,
|
||||
"rx_errors": 4,
|
||||
"rx_packets": 73564835,
|
||||
"snr": [30, 30, 30, 30],
|
||||
"speed": 1000,
|
||||
"tx_bytes": 209900085624,
|
||||
"tx_dropped": 10,
|
||||
"tx_errors": 0,
|
||||
"tx_packets": 185866883
|
||||
}
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"hwaddr": "01:23:45:67:89:AB",
|
||||
"ifname": "ath0",
|
||||
"mtu": 1500,
|
||||
"status": {
|
||||
"cable_len": null,
|
||||
"duplex": false,
|
||||
"ip6addr": null,
|
||||
"ipaddr": "0.0.0.0",
|
||||
"plugged": false,
|
||||
"rx_bytes": 206938324766,
|
||||
"rx_dropped": 0,
|
||||
"rx_errors": 0,
|
||||
"rx_packets": 149767200,
|
||||
"snr": null,
|
||||
"speed": 0,
|
||||
"tx_bytes": 5265602738,
|
||||
"tx_dropped": 2005,
|
||||
"tx_errors": 0,
|
||||
"tx_packets": 52980390
|
||||
}
|
||||
},
|
||||
{
|
||||
"enabled": true,
|
||||
"hwaddr": "01:23:45:67:89:AB",
|
||||
"ifname": "br0",
|
||||
"mtu": 1500,
|
||||
"status": {
|
||||
"cable_len": null,
|
||||
"duplex": false,
|
||||
"ip6addr": [
|
||||
{
|
||||
"addr": "fe80::eea:14ff:fea4:89cd",
|
||||
"plen": 64
|
||||
}
|
||||
],
|
||||
"ipaddr": "192.168.1.2",
|
||||
"plugged": true,
|
||||
"rx_bytes": 204802727,
|
||||
"rx_dropped": 0,
|
||||
"rx_errors": 0,
|
||||
"rx_packets": 1791592,
|
||||
"snr": null,
|
||||
"speed": 0,
|
||||
"tx_bytes": 236295176,
|
||||
"tx_dropped": 0,
|
||||
"tx_errors": 0,
|
||||
"tx_packets": 298119
|
||||
}
|
||||
}
|
||||
],
|
||||
"ntpclient": {},
|
||||
"portfw": false,
|
||||
"provmode": {},
|
||||
"services": {
|
||||
"airview": 2,
|
||||
"dhcp6d_stateful": false,
|
||||
"dhcpc": false,
|
||||
"dhcpd": false,
|
||||
"pppoe": false
|
||||
},
|
||||
"unms": {
|
||||
"status": 0,
|
||||
"timestamp": null
|
||||
},
|
||||
"wireless": {
|
||||
"essid": "DemoSSID",
|
||||
"mode": "ap-ptp",
|
||||
"ieeemode": "11ACVHT80",
|
||||
"band": 2,
|
||||
"compat_11n": 0,
|
||||
"hide_essid": 0,
|
||||
"apmac": "01:23:45:67:89:AB",
|
||||
"antenna_gain": 13,
|
||||
"frequency": 5500,
|
||||
"center1_freq": 5530,
|
||||
"dfs": 1,
|
||||
"distance": 0,
|
||||
"security": "WPA2",
|
||||
"noisef": -89,
|
||||
"txpower": -3,
|
||||
"apmac": "01:23:45:67:89:AB",
|
||||
"aprepeater": false,
|
||||
"rstatus": 5,
|
||||
"chanbw": 80,
|
||||
"rx_chainmask": 3,
|
||||
"tx_chainmask": 3,
|
||||
"nol_state": 0,
|
||||
"nol_timeout": 0,
|
||||
"band": 2,
|
||||
"cac_state": 0,
|
||||
"cac_timeout": 0,
|
||||
"rx_idx": 8,
|
||||
"rx_nss": 2,
|
||||
"tx_idx": 9,
|
||||
"tx_nss": 2,
|
||||
"throughput": { "tx": 222, "rx": 9907 },
|
||||
"service": { "time": 267181, "link": 266003 },
|
||||
"center1_freq": 5530,
|
||||
"chanbw": 80,
|
||||
"compat_11n": 0,
|
||||
"count": 1,
|
||||
"dfs": 1,
|
||||
"distance": 0,
|
||||
"essid": "DemoSSID",
|
||||
"frequency": 5500,
|
||||
"hide_essid": 0,
|
||||
"ieeemode": "11ACVHT80",
|
||||
"mode": "ap-ptp",
|
||||
"noisef": -89,
|
||||
"nol_state": 0,
|
||||
"nol_timeout": 0,
|
||||
"polling": {
|
||||
"atpc_status": 2,
|
||||
"cb_capacity": 593970,
|
||||
"dl_capacity": 647400,
|
||||
"ul_capacity": 540540,
|
||||
"use": 48,
|
||||
"tx_use": 6,
|
||||
"rx_use": 42,
|
||||
"atpc_status": 2,
|
||||
"ff_cap_rep": false,
|
||||
"fixed_frame": false,
|
||||
"flex_mode": null,
|
||||
"gps_sync": false,
|
||||
"ff_cap_rep": false
|
||||
"rx_use": 42,
|
||||
"tx_use": 6,
|
||||
"ul_capacity": 540540,
|
||||
"use": 48
|
||||
},
|
||||
"rstatus": 5,
|
||||
"rx_chainmask": 3,
|
||||
"rx_idx": 8,
|
||||
"rx_nss": 2,
|
||||
"security": "WPA2",
|
||||
"service": {
|
||||
"link": 266003,
|
||||
"time": 267181
|
||||
},
|
||||
"count": 1,
|
||||
"sta": [
|
||||
{
|
||||
"mac": "01:23:45:67:89:AB",
|
||||
"lastip": "192.168.1.2",
|
||||
"signal": -59,
|
||||
"rssi": 37,
|
||||
"noisefloor": -89,
|
||||
"chainrssi": [35, 32, 0],
|
||||
"tx_idx": 9,
|
||||
"rx_idx": 8,
|
||||
"tx_nss": 2,
|
||||
"rx_nss": 2,
|
||||
"tx_latency": 0,
|
||||
"distance": 1,
|
||||
"tx_packets": 0,
|
||||
"tx_lretries": 0,
|
||||
"tx_sretries": 0,
|
||||
"uptime": 170281,
|
||||
"dl_signal_expect": -80,
|
||||
"ul_signal_expect": -55,
|
||||
"cb_capacity_expect": 416000,
|
||||
"dl_capacity_expect": 208000,
|
||||
"ul_capacity_expect": 624000,
|
||||
"dl_rate_expect": 3,
|
||||
"ul_rate_expect": 8,
|
||||
"dl_linkscore": 100,
|
||||
"ul_linkscore": 86,
|
||||
"dl_avg_linkscore": 100,
|
||||
"ul_avg_linkscore": 88,
|
||||
"tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430],
|
||||
"stats": {
|
||||
"rx_bytes": 206938324814,
|
||||
"rx_packets": 149767200,
|
||||
"rx_pps": 846,
|
||||
"tx_bytes": 5265602739,
|
||||
"tx_packets": 52980390,
|
||||
"tx_pps": 0
|
||||
},
|
||||
"airmax": {
|
||||
"actual_priority": 0,
|
||||
"beam": 0,
|
||||
"desired_priority": 0,
|
||||
"cb_capacity": 593970,
|
||||
"dl_capacity": 647400,
|
||||
"ul_capacity": 540540,
|
||||
"atpc_status": 2,
|
||||
"beam": 0,
|
||||
"cb_capacity": 593970,
|
||||
"desired_priority": 0,
|
||||
"dl_capacity": 647400,
|
||||
"rx": {
|
||||
"usage": 42,
|
||||
"cinr": 31,
|
||||
"evm": [
|
||||
[
|
||||
@@ -141,10 +203,10 @@
|
||||
34, 33, 34, 34, 34, 34, 34, 35, 35, 35, 34, 35, 33, 34, 34, 34,
|
||||
34, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35
|
||||
]
|
||||
]
|
||||
],
|
||||
"usage": 42
|
||||
},
|
||||
"tx": {
|
||||
"usage": 6,
|
||||
"cinr": 31,
|
||||
"evm": [
|
||||
[
|
||||
@@ -159,142 +221,127 @@
|
||||
38, 37, 37, 37, 38, 37, 38, 37, 37, 37, 37, 37, 36, 37, 37, 37,
|
||||
37, 37, 37, 38, 37, 37, 38, 37, 36, 37, 37, 37, 37, 37, 37, 37
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"usage": 6
|
||||
},
|
||||
"ul_capacity": 540540
|
||||
},
|
||||
"airos_connected": true,
|
||||
"cb_capacity_expect": 416000,
|
||||
"chainrssi": [35, 32, 0],
|
||||
"distance": 1,
|
||||
"dl_avg_linkscore": 100,
|
||||
"dl_capacity_expect": 208000,
|
||||
"dl_linkscore": 100,
|
||||
"dl_rate_expect": 3,
|
||||
"dl_signal_expect": -80,
|
||||
"last_disc": 1,
|
||||
"lastip": "192.168.1.2",
|
||||
"mac": "01:23:45:67:89:AB",
|
||||
"noisefloor": -89,
|
||||
"remote": {
|
||||
"age": 1,
|
||||
"device_id": "d4f4cdf82961e619328a8f72f8d7653b",
|
||||
"hostname": "NanoStation 5AC sta name",
|
||||
"platform": "NanoStation 5AC loco",
|
||||
"version": "WA.ar934x.v8.7.17.48152.250620.2132",
|
||||
"time": "2025-06-23 23:13:54",
|
||||
"cpuload": 43.564301,
|
||||
"temperature": 0,
|
||||
"totalram": 63447040,
|
||||
"freeram": 14290944,
|
||||
"netrole": "bridge",
|
||||
"mode": "sta-ptp",
|
||||
"sys_id": "0xe7fa",
|
||||
"tx_throughput": 16023,
|
||||
"rx_throughput": 251,
|
||||
"uptime": 265320,
|
||||
"power_time": 268512,
|
||||
"compat_11n": 0,
|
||||
"signal": -58,
|
||||
"rssi": 38,
|
||||
"noisefloor": -90,
|
||||
"tx_power": -4,
|
||||
"distance": 1,
|
||||
"rx_chainmask": 3,
|
||||
"airview": 2,
|
||||
"antenna_gain": 13,
|
||||
"cable_loss": 0,
|
||||
"chainrssi": [33, 37, 0],
|
||||
"compat_11n": 0,
|
||||
"cpuload": 43.564301,
|
||||
"device_id": "d4f4cdf82961e619328a8f72f8d7653b",
|
||||
"distance": 1,
|
||||
"ethlist": [
|
||||
{
|
||||
"cable_len": 14,
|
||||
"duplex": true,
|
||||
"enabled": true,
|
||||
"ifname": "eth0",
|
||||
"plugged": true,
|
||||
"snr": [30, 30, 29, 30],
|
||||
"speed": 1000
|
||||
}
|
||||
],
|
||||
"freeram": 14290944,
|
||||
"gps": {
|
||||
"alt": null,
|
||||
"dim": null,
|
||||
"dop": null,
|
||||
"fix": 0,
|
||||
"lat": 52.379894,
|
||||
"lon": 4.901608,
|
||||
"sats": null,
|
||||
"time_synced": null
|
||||
},
|
||||
"height": 2,
|
||||
"hostname": "NanoStation 5AC sta name",
|
||||
"ip6addr": ["fe80::eea:14ff:fea4:89ab"],
|
||||
"ipaddr": ["192.168.1.2"],
|
||||
"mode": "sta-ptp",
|
||||
"netrole": "bridge",
|
||||
"noisefloor": -90,
|
||||
"oob": false,
|
||||
"platform": "NanoStation 5AC loco",
|
||||
"power_time": 268512,
|
||||
"rssi": 38,
|
||||
"rx_bytes": 3624206478,
|
||||
"rx_chainmask": 3,
|
||||
"rx_throughput": 251,
|
||||
"service": {
|
||||
"link": 265996,
|
||||
"time": 267195
|
||||
},
|
||||
"signal": -58,
|
||||
"sys_id": "0xe7fa",
|
||||
"temperature": 0,
|
||||
"time": "2025-06-23 23:13:54",
|
||||
"totalram": 63447040,
|
||||
"tx_bytes": 212308148210,
|
||||
"tx_power": -4,
|
||||
"tx_ratedata": [
|
||||
14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154
|
||||
],
|
||||
"tx_bytes": 212308148210,
|
||||
"rx_bytes": 3624206478,
|
||||
"antenna_gain": 13,
|
||||
"cable_loss": 0,
|
||||
"height": 2,
|
||||
"ethlist": [
|
||||
{
|
||||
"ifname": "eth0",
|
||||
"enabled": true,
|
||||
"plugged": true,
|
||||
"duplex": true,
|
||||
"speed": 1000,
|
||||
"snr": [30, 30, 29, 30],
|
||||
"cable_len": 14
|
||||
}
|
||||
],
|
||||
"ipaddr": ["192.168.1.2"],
|
||||
"ip6addr": ["fe80::eea:14ff:fea4:89ab"],
|
||||
"gps": { "lat": "52.379894", "lon": "4.901608", "fix": 0 },
|
||||
"oob": false,
|
||||
"unms": { "status": 0, "timestamp": null },
|
||||
"airview": 2,
|
||||
"service": { "time": 267195, "link": 265996 }
|
||||
"tx_throughput": 16023,
|
||||
"unms": {
|
||||
"status": 0,
|
||||
"timestamp": null
|
||||
},
|
||||
"uptime": 265320,
|
||||
"version": "WA.ar934x.v8.7.17.48152.250620.2132"
|
||||
},
|
||||
"airos_connected": true
|
||||
"rssi": 37,
|
||||
"rx_idx": 8,
|
||||
"rx_nss": 2,
|
||||
"signal": -59,
|
||||
"stats": {
|
||||
"rx_bytes": 206938324814,
|
||||
"rx_packets": 149767200,
|
||||
"rx_pps": 846,
|
||||
"tx_bytes": 5265602739,
|
||||
"tx_packets": 52980390,
|
||||
"tx_pps": 0
|
||||
},
|
||||
"tx_idx": 9,
|
||||
"tx_latency": 0,
|
||||
"tx_lretries": 0,
|
||||
"tx_nss": 2,
|
||||
"tx_packets": 0,
|
||||
"tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430],
|
||||
"tx_sretries": 0,
|
||||
"ul_avg_linkscore": 88,
|
||||
"ul_capacity_expect": 624000,
|
||||
"ul_linkscore": 86,
|
||||
"ul_rate_expect": 8,
|
||||
"ul_signal_expect": -55,
|
||||
"uptime": 170281
|
||||
}
|
||||
],
|
||||
"sta_disconnected": []
|
||||
},
|
||||
"interfaces": [
|
||||
{
|
||||
"ifname": "eth0",
|
||||
"hwaddr": "01:23:45:67:89:AB",
|
||||
"enabled": true,
|
||||
"mtu": 1500,
|
||||
"status": {
|
||||
"plugged": true,
|
||||
"tx_bytes": 209900085624,
|
||||
"rx_bytes": 3984971949,
|
||||
"tx_packets": 185866883,
|
||||
"rx_packets": 73564835,
|
||||
"tx_errors": 0,
|
||||
"rx_errors": 4,
|
||||
"tx_dropped": 10,
|
||||
"rx_dropped": 0,
|
||||
"ipaddr": "0.0.0.0",
|
||||
"speed": 1000,
|
||||
"duplex": true,
|
||||
"snr": [30, 30, 30, 30],
|
||||
"cable_len": 18,
|
||||
"ip6addr": null
|
||||
}
|
||||
"sta_disconnected": [],
|
||||
"throughput": {
|
||||
"rx": 9907,
|
||||
"tx": 222
|
||||
},
|
||||
{
|
||||
"ifname": "ath0",
|
||||
"hwaddr": "01:23:45:67:89:AB",
|
||||
"enabled": true,
|
||||
"mtu": 1500,
|
||||
"status": {
|
||||
"plugged": false,
|
||||
"tx_bytes": 5265602738,
|
||||
"rx_bytes": 206938324766,
|
||||
"tx_packets": 52980390,
|
||||
"rx_packets": 149767200,
|
||||
"tx_errors": 0,
|
||||
"rx_errors": 0,
|
||||
"tx_dropped": 2005,
|
||||
"rx_dropped": 0,
|
||||
"ipaddr": "0.0.0.0",
|
||||
"speed": 0,
|
||||
"duplex": false,
|
||||
"snr": null,
|
||||
"cable_len": null,
|
||||
"ip6addr": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"ifname": "br0",
|
||||
"hwaddr": "01:23:45:67:89:AB",
|
||||
"enabled": true,
|
||||
"mtu": 1500,
|
||||
"status": {
|
||||
"plugged": true,
|
||||
"tx_bytes": 236295176,
|
||||
"rx_bytes": 204802727,
|
||||
"tx_packets": 298119,
|
||||
"rx_packets": 1791592,
|
||||
"tx_errors": 0,
|
||||
"rx_errors": 0,
|
||||
"tx_dropped": 0,
|
||||
"rx_dropped": 0,
|
||||
"ipaddr": "192.168.1.2",
|
||||
"speed": 0,
|
||||
"duplex": false,
|
||||
"snr": null,
|
||||
"cable_len": null,
|
||||
"ip6addr": [{ "addr": "fe80::eea:14ff:fea4:89cd", "plen": 64 }]
|
||||
}
|
||||
}
|
||||
],
|
||||
"provmode": {},
|
||||
"ntpclient": {},
|
||||
"unms": { "status": 0, "timestamp": null },
|
||||
"gps": { "lat": 52.379894, "lon": 4.901608, "fix": 0 },
|
||||
"derived": { "mac": "01:23:45:67:89:AB", "mac_interface": "br0" }
|
||||
"tx_chainmask": 3,
|
||||
"tx_idx": 9,
|
||||
"tx_nss": 2,
|
||||
"txpower": -3
|
||||
}
|
||||
}
|
@@ -13,8 +13,12 @@
|
||||
}),
|
||||
]),
|
||||
'derived': dict({
|
||||
'access_point': True,
|
||||
'mac': '**REDACTED**',
|
||||
'mac_interface': 'br0',
|
||||
'ptmp': False,
|
||||
'ptp': True,
|
||||
'station': False,
|
||||
}),
|
||||
'firewall': dict({
|
||||
'eb6tables': False,
|
||||
@@ -164,6 +168,7 @@
|
||||
'dl_capacity': 647400,
|
||||
'ff_cap_rep': False,
|
||||
'fixed_frame': False,
|
||||
'flex_mode': None,
|
||||
'gps_sync': False,
|
||||
'rx_use': 42,
|
||||
'tx_use': 6,
|
||||
@@ -515,9 +520,14 @@
|
||||
]),
|
||||
'freeram': 14290944,
|
||||
'gps': dict({
|
||||
'alt': None,
|
||||
'dim': None,
|
||||
'dop': None,
|
||||
'fix': 0,
|
||||
'lat': '**REDACTED**',
|
||||
'lon': '**REDACTED**',
|
||||
'sats': None,
|
||||
'time_synced': None,
|
||||
}),
|
||||
'height': 2,
|
||||
'hostname': '**REDACTED**',
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from copy import deepcopy
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -395,6 +396,15 @@ async def test_status_with_deprecated_battery_feature(
|
||||
assert issue.issue_domain == "vacuum"
|
||||
assert issue.translation_key == "deprecated_vacuum_battery_feature"
|
||||
assert issue.translation_placeholders == {"entity_id": "vacuum.mqtttest"}
|
||||
assert not [
|
||||
record
|
||||
for record in caplog.records
|
||||
if record.name == "homeassistant.helpers.frame"
|
||||
and record.levelno >= logging.WARNING
|
||||
]
|
||||
assert (
|
||||
"mqtt' is setting the battery_level which has been deprecated"
|
||||
) not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@@ -94,7 +94,7 @@ def mock_config_entry_with_reasoning_model(
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
next(iter(mock_config_entry.subentries.values())),
|
||||
data={CONF_LLM_HASS_API: llm.LLM_API_ASSIST, CONF_CHAT_MODEL: "o4-mini"},
|
||||
data={CONF_LLM_HASS_API: llm.LLM_API_ASSIST, CONF_CHAT_MODEL: "gpt-5-mini"},
|
||||
)
|
||||
return mock_config_entry
|
||||
|
||||
|
@@ -20,6 +20,7 @@ from homeassistant.components.openai_conversation.const import (
|
||||
CONF_RECOMMENDED,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TOP_P,
|
||||
CONF_VERBOSITY,
|
||||
CONF_WEB_SEARCH,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE,
|
||||
@@ -302,7 +303,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_PROMPT: "Speak like a pro",
|
||||
},
|
||||
{
|
||||
CONF_TEMPERATURE: 1.0,
|
||||
@@ -317,7 +318,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
),
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_PROMPT: "Speak like a pro",
|
||||
CONF_TEMPERATURE: 1.0,
|
||||
CONF_CHAT_MODEL: "o1-pro",
|
||||
CONF_TOP_P: RECOMMENDED_TOP_P,
|
||||
@@ -414,35 +415,51 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
( # Case 2: reasoning model
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pro",
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_TEMPERATURE: 0.8,
|
||||
CONF_CHAT_MODEL: "o1-pro",
|
||||
CONF_CHAT_MODEL: "gpt-5",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_REASONING_EFFORT: "high",
|
||||
CONF_REASONING_EFFORT: "low",
|
||||
CONF_VERBOSITY: "high",
|
||||
CONF_CODE_INTERPRETER: False,
|
||||
CONF_WEB_SEARCH: False,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "low",
|
||||
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||
},
|
||||
(
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pro",
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
},
|
||||
{
|
||||
CONF_TEMPERATURE: 0.8,
|
||||
CONF_CHAT_MODEL: "o1-pro",
|
||||
CONF_CHAT_MODEL: "gpt-5",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
},
|
||||
{CONF_REASONING_EFFORT: "high", CONF_CODE_INTERPRETER: False},
|
||||
{
|
||||
CONF_REASONING_EFFORT: "minimal",
|
||||
CONF_CODE_INTERPRETER: False,
|
||||
CONF_VERBOSITY: "high",
|
||||
CONF_WEB_SEARCH: False,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "low",
|
||||
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||
},
|
||||
),
|
||||
{
|
||||
CONF_RECOMMENDED: False,
|
||||
CONF_PROMPT: "Speak like a pro",
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_TEMPERATURE: 0.8,
|
||||
CONF_CHAT_MODEL: "o1-pro",
|
||||
CONF_CHAT_MODEL: "gpt-5",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_REASONING_EFFORT: "high",
|
||||
CONF_REASONING_EFFORT: "minimal",
|
||||
CONF_CODE_INTERPRETER: False,
|
||||
CONF_VERBOSITY: "high",
|
||||
CONF_WEB_SEARCH: False,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE: "low",
|
||||
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||
},
|
||||
),
|
||||
# Test that old options are removed after reconfiguration
|
||||
@@ -482,11 +499,13 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_TEMPERATURE: 0.8,
|
||||
CONF_CHAT_MODEL: "gpt-4o",
|
||||
CONF_CHAT_MODEL: "gpt-5",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_REASONING_EFFORT: "high",
|
||||
CONF_CODE_INTERPRETER: True,
|
||||
CONF_VERBOSITY: "low",
|
||||
CONF_WEB_SEARCH: False,
|
||||
},
|
||||
(
|
||||
{
|
||||
@@ -550,11 +569,12 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non
|
||||
CONF_PROMPT: "Speak like a pirate",
|
||||
CONF_LLM_HASS_API: ["assist"],
|
||||
CONF_TEMPERATURE: 0.8,
|
||||
CONF_CHAT_MODEL: "o3-mini",
|
||||
CONF_CHAT_MODEL: "o5",
|
||||
CONF_TOP_P: 0.9,
|
||||
CONF_MAX_TOKENS: 1000,
|
||||
CONF_REASONING_EFFORT: "low",
|
||||
CONF_CODE_INTERPRETER: True,
|
||||
CONF_VERBOSITY: "medium",
|
||||
},
|
||||
(
|
||||
{
|
||||
|
@@ -12,9 +12,9 @@ from tests.common import MockConfigEntry
|
||||
|
||||
mock_value_step_user = {
|
||||
"title": "1R & 1IN Board",
|
||||
"relay_count": 1,
|
||||
"input_count": 1,
|
||||
"is_old": False,
|
||||
"relays": 1,
|
||||
"inputs": 1,
|
||||
"temps": False,
|
||||
}
|
||||
|
||||
|
||||
|
@@ -882,3 +882,23 @@ def ungroup_speakers(coordinator: MockSoCo, group_member: MockSoCo) -> None:
|
||||
)
|
||||
coordinator.zoneGroupTopology.subscribe.return_value._callback(event)
|
||||
group_member.zoneGroupTopology.subscribe.return_value._callback(event)
|
||||
|
||||
|
||||
def create_rendering_control_event(
|
||||
soco: MockSoCo,
|
||||
) -> SonosMockEvent:
|
||||
"""Create a Sonos Event for speaker rendering control."""
|
||||
variables = {
|
||||
"dialog_level": 1,
|
||||
"speech_enhance_enable": 1,
|
||||
"surround_level": 6,
|
||||
"music_surround_level": 4,
|
||||
"audio_delay": 0,
|
||||
"audio_delay_left_rear": 0,
|
||||
"audio_delay_right_rear": 0,
|
||||
"night_mode": 0,
|
||||
"surround_enabled": 1,
|
||||
"surround_mode": 1,
|
||||
"height_channel_level": 1,
|
||||
}
|
||||
return SonosMockEvent(soco, soco.renderingControl, variables)
|
||||
|
@@ -6,13 +6,18 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER
|
||||
from homeassistant.components.sonos.const import (
|
||||
DATA_SONOS_DISCOVERY_MANAGER,
|
||||
MODEL_SONOS_ARC_ULTRA,
|
||||
)
|
||||
from homeassistant.components.sonos.switch import (
|
||||
ATTR_DURATION,
|
||||
ATTR_ID,
|
||||
ATTR_INCLUDE_LINKED_ZONES,
|
||||
ATTR_PLAY_MODE,
|
||||
ATTR_RECURRENCE,
|
||||
ATTR_SPEECH_ENHANCEMENT,
|
||||
ATTR_SPEECH_ENHANCEMENT_ENABLED,
|
||||
ATTR_VOLUME,
|
||||
)
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
@@ -29,7 +34,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import MockSoCo, SonosMockEvent
|
||||
from .conftest import MockSoCo, SonosMockEvent, create_rendering_control_event
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
@@ -142,6 +147,49 @@ async def test_switch_attributes(
|
||||
assert touch_controls_state.state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "attribute"),
|
||||
[
|
||||
("Sonos One SL", ATTR_SPEECH_ENHANCEMENT),
|
||||
(MODEL_SONOS_ARC_ULTRA.lower(), ATTR_SPEECH_ENHANCEMENT_ENABLED),
|
||||
],
|
||||
)
|
||||
async def test_switch_speech_enhancement(
|
||||
hass: HomeAssistant,
|
||||
async_setup_sonos,
|
||||
soco: MockSoCo,
|
||||
speaker_info: dict[str, str],
|
||||
entity_registry: er.EntityRegistry,
|
||||
model: str,
|
||||
attribute: str,
|
||||
) -> None:
|
||||
"""Tests the speech enhancement switch and attribute substitution for different models."""
|
||||
entity_id = "switch.zone_a_speech_enhancement"
|
||||
speaker_info["model_name"] = model
|
||||
soco.get_speaker_info.return_value = speaker_info
|
||||
setattr(soco, attribute, True)
|
||||
await async_setup_sonos()
|
||||
switch = entity_registry.entities[entity_id]
|
||||
state = hass.states.get(switch.entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
event = create_rendering_control_event(soco)
|
||||
event.variables[attribute] = False
|
||||
soco.renderingControl.subscribe.return_value._callback(event)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(switch.entity_id)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
assert getattr(soco, attribute) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "expected_result"),
|
||||
[
|
||||
|
@@ -114,6 +114,7 @@ DEVICE_MOCKS = {
|
||||
"kj_CAjWAxBUZt7QZHfz": [
|
||||
# https://github.com/home-assistant/core/issues/146023
|
||||
Platform.FAN,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
],
|
||||
"kj_yrzylxax1qspdgpp": [
|
||||
|
@@ -1649,6 +1649,58 @@
|
||||
'state': '220.4',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][sensor.hl400_pm2_5-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.hl400_pm2_5',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM2.5',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'pm25',
|
||||
'unique_id': 'tuya.152027113c6105cce49cpm25',
|
||||
'unit_of_measurement': '',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][sensor.hl400_pm2_5-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HL400 PM2.5',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hl400_pm2_5',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '45.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@@ -5,9 +5,10 @@ from __future__ import annotations
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from uiprotect import NotAuthorized, NvrError, ProtectApiClient
|
||||
from uiprotect import NvrError, ProtectApiClient
|
||||
from uiprotect.api import DEVICE_UPDATE_INTERVAL
|
||||
from uiprotect.data import NVR, Bootstrap, CloudAccount, Light
|
||||
from uiprotect.exceptions import BadRequest, NotAuthorized
|
||||
|
||||
from homeassistant.components.unifiprotect.const import (
|
||||
AUTH_RETRIES,
|
||||
@@ -414,6 +415,28 @@ async def test_setup_handles_api_key_creation_failure(
|
||||
ufp.api.set_api_key.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True)
|
||||
async def test_setup_handles_api_key_creation_bad_request(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock
|
||||
) -> None:
|
||||
"""Test handling of API key creation BadRequest error."""
|
||||
# Setup: API key is not set, user has write permissions, but creation fails with BadRequest
|
||||
ufp.api.is_api_key_set.return_value = False
|
||||
ufp.api.create_api_key = AsyncMock(
|
||||
side_effect=BadRequest("Invalid API key creation request")
|
||||
)
|
||||
|
||||
# Should fail with auth error due to API key creation failure
|
||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ufp.entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
# Verify API key creation was attempted but set_api_key was not called
|
||||
ufp.api.create_api_key.assert_called_once_with(name="Home Assistant (test home)")
|
||||
ufp.api.set_api_key.assert_not_called()
|
||||
|
||||
|
||||
async def test_setup_with_existing_api_key(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture
|
||||
) -> None:
|
||||
|
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
import logging
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
@@ -437,11 +438,13 @@ async def test_vacuum_deprecated_state_does_not_break_state(
|
||||
assert state.state == "cleaning"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_as_custom_component")
|
||||
async def test_vacuum_log_deprecated_battery_properties(
|
||||
@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)])
|
||||
async def test_vacuum_log_deprecated_battery_using_properties(
|
||||
hass: HomeAssistant,
|
||||
config_flow_fixture: None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
is_built_in: bool,
|
||||
log_warnings: int,
|
||||
) -> None:
|
||||
"""Test incorrectly using battery properties logs warning."""
|
||||
|
||||
@@ -449,7 +452,7 @@ async def test_vacuum_log_deprecated_battery_properties(
|
||||
"""Mocked vacuum entity."""
|
||||
|
||||
@property
|
||||
def activity(self) -> str:
|
||||
def activity(self) -> VacuumActivity:
|
||||
"""Return the state of the entity."""
|
||||
return VacuumActivity.CLEANING
|
||||
|
||||
@@ -477,7 +480,7 @@ async def test_vacuum_log_deprecated_battery_properties(
|
||||
async_setup_entry=help_async_setup_entry_init,
|
||||
async_unload_entry=help_async_unload_entry,
|
||||
),
|
||||
built_in=False,
|
||||
built_in=is_built_in,
|
||||
)
|
||||
setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -486,26 +489,27 @@ async def test_vacuum_log_deprecated_battery_properties(
|
||||
assert state is not None
|
||||
|
||||
assert (
|
||||
"Detected that custom integration 'test' is setting the battery_icon which has been deprecated."
|
||||
" Integration test should implement a sensor instead with a correct device class and link it"
|
||||
" to the same device. This will stop working in Home Assistant 2026.8,"
|
||||
" please report it to the author of the 'test' custom integration"
|
||||
in caplog.text
|
||||
len([record for record in caplog.records if record.levelno >= logging.WARNING])
|
||||
== log_warnings
|
||||
)
|
||||
|
||||
assert (
|
||||
"Detected that custom integration 'test' is setting the battery_level which has been deprecated."
|
||||
" Integration test should implement a sensor instead with a correct device class and link it"
|
||||
" to the same device. This will stop working in Home Assistant 2026.8,"
|
||||
" please report it to the author of the 'test' custom integration"
|
||||
"integration 'test' is setting the battery_icon which has been deprecated."
|
||||
in caplog.text
|
||||
)
|
||||
) != is_built_in
|
||||
assert (
|
||||
"integration 'test' is setting the battery_level which has been deprecated."
|
||||
in caplog.text
|
||||
) != is_built_in
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_as_custom_component")
|
||||
async def test_vacuum_log_deprecated_battery_properties_using_attr(
|
||||
@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)])
|
||||
async def test_vacuum_log_deprecated_battery_using_attr(
|
||||
hass: HomeAssistant,
|
||||
config_flow_fixture: None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
is_built_in: bool,
|
||||
log_warnings: int,
|
||||
) -> None:
|
||||
"""Test incorrectly using _attr_battery_* attribute does log issue and raise repair."""
|
||||
|
||||
@@ -531,7 +535,7 @@ async def test_vacuum_log_deprecated_battery_properties_using_attr(
|
||||
async_setup_entry=help_async_setup_entry_init,
|
||||
async_unload_entry=help_async_unload_entry,
|
||||
),
|
||||
built_in=False,
|
||||
built_in=is_built_in,
|
||||
)
|
||||
setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -541,47 +545,51 @@ async def test_vacuum_log_deprecated_battery_properties_using_attr(
|
||||
entity.start()
|
||||
|
||||
assert (
|
||||
"Detected that custom integration 'test' is setting the battery_level which has been deprecated."
|
||||
" Integration test should implement a sensor instead with a correct device class and link it to"
|
||||
" the same device. This will stop working in Home Assistant 2026.8,"
|
||||
" please report it to the author of the 'test' custom integration"
|
||||
in caplog.text
|
||||
len([record for record in caplog.records if record.levelno >= logging.WARNING])
|
||||
== log_warnings
|
||||
)
|
||||
|
||||
assert (
|
||||
"Detected that custom integration 'test' is setting the battery_icon which has been deprecated."
|
||||
" Integration test should implement a sensor instead with a correct device class and link it to"
|
||||
" the same device. This will stop working in Home Assistant 2026.8,"
|
||||
" please report it to the author of the 'test' custom integration"
|
||||
"integration 'test' is setting the battery_level which has been deprecated."
|
||||
in caplog.text
|
||||
)
|
||||
) != is_built_in
|
||||
assert (
|
||||
"integration 'test' is setting the battery_icon which has been deprecated."
|
||||
in caplog.text
|
||||
) != is_built_in
|
||||
|
||||
await async_start(hass, entity.entity_id)
|
||||
|
||||
caplog.clear()
|
||||
|
||||
await async_start(hass, entity.entity_id)
|
||||
|
||||
# Test we only log once
|
||||
assert (
|
||||
"Detected that custom integration 'test' is setting the battery_level which has been deprecated."
|
||||
not in caplog.text
|
||||
)
|
||||
assert (
|
||||
"Detected that custom integration 'test' is setting the battery_icon which has been deprecated."
|
||||
not in caplog.text
|
||||
len([record for record in caplog.records if record.levelno >= logging.WARNING])
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_as_custom_component")
|
||||
@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 1)])
|
||||
async def test_vacuum_log_deprecated_battery_supported_feature(
|
||||
hass: HomeAssistant,
|
||||
config_flow_fixture: None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
is_built_in: bool,
|
||||
log_warnings: int,
|
||||
) -> None:
|
||||
"""Test incorrectly setting battery supported feature logs warning."""
|
||||
|
||||
entity = MockVacuum(
|
||||
name="Testing",
|
||||
entity_id="vacuum.test",
|
||||
)
|
||||
class MockVacuum(StateVacuumEntity):
|
||||
"""Mock vacuum class."""
|
||||
|
||||
_attr_supported_features = (
|
||||
VacuumEntityFeature.STATE | VacuumEntityFeature.BATTERY
|
||||
)
|
||||
_attr_name = "Testing"
|
||||
|
||||
entity = MockVacuum()
|
||||
config_entry = MockConfigEntry(domain="test")
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
@@ -592,7 +600,7 @@ async def test_vacuum_log_deprecated_battery_supported_feature(
|
||||
async_setup_entry=help_async_setup_entry_init,
|
||||
async_unload_entry=help_async_unload_entry,
|
||||
),
|
||||
built_in=False,
|
||||
built_in=is_built_in,
|
||||
)
|
||||
setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -601,13 +609,14 @@ async def test_vacuum_log_deprecated_battery_supported_feature(
|
||||
assert state is not None
|
||||
|
||||
assert (
|
||||
"Detected that custom integration 'test' is setting the battery supported feature"
|
||||
" which has been deprecated. Integration test should remove this as part of migrating"
|
||||
" the battery level and icon to a sensor. This will stop working in Home Assistant 2026.8"
|
||||
", please report it to the author of the 'test' custom integration"
|
||||
in caplog.text
|
||||
len([record for record in caplog.records if record.levelno >= logging.WARNING])
|
||||
== log_warnings
|
||||
)
|
||||
|
||||
assert (
|
||||
"integration 'test' is setting the battery supported feature" in caplog.text
|
||||
) != is_built_in
|
||||
|
||||
|
||||
async def test_vacuum_not_log_deprecated_battery_properties_during_init(
|
||||
hass: HomeAssistant,
|
||||
@@ -624,7 +633,7 @@ async def test_vacuum_not_log_deprecated_battery_properties_during_init(
|
||||
self._attr_battery_level = 50
|
||||
|
||||
@property
|
||||
def activity(self) -> str:
|
||||
def activity(self) -> VacuumActivity:
|
||||
"""Return the state of the entity."""
|
||||
return VacuumActivity.CLEANING
|
||||
|
||||
@@ -635,6 +644,6 @@ async def test_vacuum_not_log_deprecated_battery_properties_during_init(
|
||||
assert entity.battery_level == 50
|
||||
|
||||
assert (
|
||||
"Detected that custom integration 'test' is setting the battery_level which has been deprecated."
|
||||
not in caplog.text
|
||||
len([record for record in caplog.records if record.levelno >= logging.WARNING])
|
||||
== 0
|
||||
)
|
||||
|
@@ -20,6 +20,12 @@ _MODEL_SPECIFIC_RESPONSES = {
|
||||
"statistics",
|
||||
"vehicle",
|
||||
],
|
||||
"xc60_phev_2020": [
|
||||
"energy_capabilities",
|
||||
"energy_state",
|
||||
"statistics",
|
||||
"vehicle",
|
||||
],
|
||||
"xc90_petrol_2019": ["commands", "statistics", "vehicle"],
|
||||
}
|
||||
|
||||
|
@@ -1,57 +1,56 @@
|
||||
{
|
||||
"batteryChargeLevel": {
|
||||
"status": "OK",
|
||||
"value": 38,
|
||||
"value": 90.0,
|
||||
"unit": "percentage",
|
||||
"updatedAt": "2025-07-02T08:51:23Z"
|
||||
"updatedAt": "2025-08-07T14:30:32Z"
|
||||
},
|
||||
"electricRange": {
|
||||
"status": "OK",
|
||||
"value": 90,
|
||||
"value": 327,
|
||||
"unit": "km",
|
||||
"updatedAt": "2025-07-02T08:51:23Z"
|
||||
"updatedAt": "2025-08-07T14:30:32Z"
|
||||
},
|
||||
"chargerConnectionStatus": {
|
||||
"status": "OK",
|
||||
"value": "DISCONNECTED",
|
||||
"updatedAt": "2025-07-02T08:51:23Z"
|
||||
"value": "CONNECTED",
|
||||
"updatedAt": "2025-08-07T14:30:32Z"
|
||||
},
|
||||
"chargingStatus": {
|
||||
"status": "OK",
|
||||
"value": "IDLE",
|
||||
"updatedAt": "2025-07-02T08:51:23Z"
|
||||
"value": "DONE",
|
||||
"updatedAt": "2025-08-07T14:30:32Z"
|
||||
},
|
||||
"chargingType": {
|
||||
"status": "OK",
|
||||
"value": "NONE",
|
||||
"updatedAt": "2025-07-02T08:51:23Z"
|
||||
"value": "AC",
|
||||
"updatedAt": "2025-08-07T14:30:32Z"
|
||||
},
|
||||
"chargerPowerStatus": {
|
||||
"status": "OK",
|
||||
"value": "NO_POWER_AVAILABLE",
|
||||
"updatedAt": "2025-07-02T08:51:23Z"
|
||||
"value": "FAULT",
|
||||
"updatedAt": "2025-08-07T14:30:32Z"
|
||||
},
|
||||
"estimatedChargingTimeToTargetBatteryChargeLevel": {
|
||||
"status": "OK",
|
||||
"value": 0,
|
||||
"value": 2,
|
||||
"unit": "minutes",
|
||||
"updatedAt": "2025-07-02T08:51:23Z"
|
||||
"updatedAt": "2025-08-07T14:30:32Z"
|
||||
},
|
||||
"chargingCurrentLimit": {
|
||||
"status": "OK",
|
||||
"value": 32,
|
||||
"unit": "ampere",
|
||||
"updatedAt": "2024-03-05T08:38:44Z"
|
||||
"status": "ERROR",
|
||||
"code": "NOT_SUPPORTED",
|
||||
"message": "Resource is not supported for this vehicle"
|
||||
},
|
||||
"targetBatteryChargeLevel": {
|
||||
"status": "OK",
|
||||
"value": 90,
|
||||
"unit": "percentage",
|
||||
"updatedAt": "2024-09-22T09:40:12Z"
|
||||
"updatedAt": "2025-08-07T14:49:50Z"
|
||||
},
|
||||
"chargingPower": {
|
||||
"status": "ERROR",
|
||||
"code": "PROPERTY_NOT_FOUND",
|
||||
"message": "No valid value could be found for the requested property"
|
||||
"code": "NOT_SUPPORTED",
|
||||
"message": "Resource is not supported for this vehicle"
|
||||
}
|
||||
}
|
||||
|
@@ -7,8 +7,8 @@
|
||||
},
|
||||
"electricRange": {
|
||||
"status": "OK",
|
||||
"value": 220,
|
||||
"unit": "km",
|
||||
"value": 150,
|
||||
"unit": "mi",
|
||||
"updatedAt": "2025-07-02T08:51:23Z"
|
||||
},
|
||||
"chargerConnectionStatus": {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
17
tests/components/volvo/fixtures/xc60_phev_2020/vehicle.json
Normal file
17
tests/components/volvo/fixtures/xc60_phev_2020/vehicle.json
Normal 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
@@ -15,7 +15,13 @@ from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"full_model",
|
||||
["ex30_2024", "s90_diesel_2018", "xc40_electric_2024", "xc90_petrol_2019"],
|
||||
[
|
||||
"ex30_2024",
|
||||
"s90_diesel_2018",
|
||||
"xc40_electric_2024",
|
||||
"xc60_phev_2020",
|
||||
"xc90_petrol_2019",
|
||||
],
|
||||
)
|
||||
async def test_sensor(
|
||||
hass: HomeAssistant,
|
||||
@@ -30,3 +36,36 @@ async def test_sensor(
|
||||
assert await setup_integration()
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"full_model",
|
||||
["xc40_electric_2024"],
|
||||
)
|
||||
async def test_distance_to_empty_battery(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: Callable[[], Awaitable[bool]],
|
||||
) -> None:
|
||||
"""Test using `distanceToEmptyBattery` instead of `electricRange`."""
|
||||
|
||||
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
|
||||
assert await setup_integration()
|
||||
|
||||
assert hass.states.get("sensor.volvo_xc40_distance_to_empty_battery").state == "250"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("full_model", "short_model"),
|
||||
[("ex30_2024", "ex30"), ("xc60_phev_2020", "xc60")],
|
||||
)
|
||||
async def test_skip_invalid_api_fields(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: Callable[[], Awaitable[bool]],
|
||||
short_model: str,
|
||||
) -> None:
|
||||
"""Test if invalid values are not creating a sensor."""
|
||||
|
||||
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
|
||||
assert await setup_integration()
|
||||
|
||||
assert not hass.states.get(f"sensor.volvo_{short_model}_charging_power")
|
||||
|
@@ -36,6 +36,7 @@
|
||||
}),
|
||||
'network_key': '**REDACTED**',
|
||||
'nwk_addresses': dict({
|
||||
'11:22:33:44:55:66:77:88': 4660,
|
||||
}),
|
||||
'nwk_manager_id': 0,
|
||||
'nwk_update_id': 0,
|
||||
|
@@ -6,6 +6,7 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from syrupy.filters import props
|
||||
from zigpy.profiles import zha
|
||||
from zigpy.types import EUI64, NWK
|
||||
from zigpy.zcl.clusters import security
|
||||
|
||||
from homeassistant.components.zha.helpers import (
|
||||
@@ -71,6 +72,10 @@ async def test_diagnostics_for_config_entry(
|
||||
|
||||
gateway.application_controller.energy_scan.side_effect = None
|
||||
gateway.application_controller.energy_scan.return_value = scan
|
||||
gateway.application_controller.state.network_info.nwk_addresses = {
|
||||
EUI64.convert("11:22:33:44:55:66:77:88"): NWK(0x1234)
|
||||
}
|
||||
|
||||
diagnostics_data = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, config_entry
|
||||
)
|
||||
|
@@ -28,7 +28,13 @@ from homeassistant.components.zwave_js.discovery_data_template import (
|
||||
DynamicCurrentTempClimateDataTemplate,
|
||||
)
|
||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, EntityCategory
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
STATE_OFF,
|
||||
STATE_UNKNOWN,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -253,6 +259,7 @@ async def test_merten_507801_disabled_enitites(
|
||||
assert updated_entry.disabled is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platforms", [[Platform.BUTTON, Platform.NUMBER]])
|
||||
async def test_zooz_zen72(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
@@ -324,6 +331,9 @@ async def test_zooz_zen72(
|
||||
assert args["value"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"platforms", [[Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]]
|
||||
)
|
||||
async def test_indicator_test(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
|
@@ -167,7 +167,7 @@ async def test_update_entity_states(
|
||||
|
||||
client.async_send_command.return_value = {"updates": []}
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -186,7 +186,7 @@ async def test_update_entity_states(
|
||||
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=2))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -224,7 +224,7 @@ async def test_update_entity_states(
|
||||
|
||||
client.async_send_command.return_value = {"updates": []}
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=3))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=3))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -246,7 +246,7 @@ async def test_update_entity_install_raises(
|
||||
"""Test update entity install raises exception."""
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test failed installation by driver
|
||||
@@ -279,7 +279,7 @@ async def test_update_entity_sleep(
|
||||
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Two nodes in total, the controller node and the zen_31 node.
|
||||
@@ -324,7 +324,7 @@ async def test_update_entity_dead(
|
||||
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Two nodes in total, the controller node and the zen_31 node.
|
||||
@@ -368,14 +368,14 @@ async def test_update_entity_ha_not_running(
|
||||
# Update should be delayed by a day because Home Assistant is not running
|
||||
hass.set_state(CoreState.starting)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert client.async_send_command.call_count == 0
|
||||
|
||||
hass.set_state(CoreState.running)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Two nodes in total, the controller node and the zen_31 node.
|
||||
@@ -401,7 +401,7 @@ async def test_update_entity_update_failure(
|
||||
assert client.async_send_command.call_count == 0
|
||||
client.async_send_command.side_effect = FailedZWaveCommand("test", 260, "test")
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_ids = (CONTROLLER_UPDATE_ENTITY, NODE_UPDATE_ENTITY)
|
||||
@@ -509,7 +509,7 @@ async def test_update_entity_progress(
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
driver = client.driver
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -657,7 +657,7 @@ async def test_update_entity_install_failed(
|
||||
driver = client.driver
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -733,7 +733,7 @@ async def test_update_entity_reload(
|
||||
|
||||
client.async_send_command.return_value = {"updates": []}
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -742,7 +742,7 @@ async def test_update_entity_reload(
|
||||
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=2))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -774,7 +774,7 @@ async def test_update_entity_reload(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Trigger another update and make sure the skipped version is still skipped
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=4))
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=4))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -809,7 +809,7 @@ async def test_update_entity_delay(
|
||||
|
||||
assert client.async_send_command.call_count == 0
|
||||
|
||||
update_interval = timedelta(minutes=5)
|
||||
update_interval = timedelta(seconds=15)
|
||||
freezer.tick(update_interval)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
Reference in New Issue
Block a user