diff --git a/homeassistant/components/airos/__init__.py b/homeassistant/components/airos/__init__.py index 54f0db205a9c35..ea184e5613d6e6 100644 --- a/homeassistant/components/airos/__init__.py +++ b/homeassistant/components/airos/__init__.py @@ -10,7 +10,10 @@ from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator -_PLATFORMS: list[Platform] = [Platform.SENSOR] +_PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.SENSOR, +] async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: diff --git a/homeassistant/components/airos/binary_sensor.py b/homeassistant/components/airos/binary_sensor.py new file mode 100644 index 00000000000000..e743cda4c63dff --- /dev/null +++ b/homeassistant/components/airos/binary_sensor.py @@ -0,0 +1,106 @@ +"""AirOS Binary Sensor component for Home Assistant.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator +from .entity import AirOSEntity + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describe an AirOS binary sensor.""" + + value_fn: Callable[[AirOSData], bool] + + +BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = ( + AirOSBinarySensorEntityDescription( + key="portfw", + translation_key="port_forwarding", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.portfw, + ), + AirOSBinarySensorEntityDescription( + key="dhcp_client", + translation_key="dhcp_client", + device_class=BinarySensorDeviceClass.RUNNING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.services.dhcpc, + ), + AirOSBinarySensorEntityDescription( + key="dhcp_server", + translation_key="dhcp_server", + device_class=BinarySensorDeviceClass.RUNNING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.services.dhcpd, + entity_registry_enabled_default=False, + ), + AirOSBinarySensorEntityDescription( + key="dhcp6_server", + translation_key="dhcp6_server", + device_class=BinarySensorDeviceClass.RUNNING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.services.dhcp6d_stateful, + entity_registry_enabled_default=False, + ), + AirOSBinarySensorEntityDescription( + key="pppoe", + translation_key="pppoe", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.services.pppoe, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AirOSConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the AirOS binary sensors from a config entry.""" + coordinator = config_entry.runtime_data + + async_add_entities( + AirOSBinarySensor(coordinator, description) for description in BINARY_SENSORS + ) + + +class AirOSBinarySensor(AirOSEntity, BinarySensorEntity): + """Representation of a binary sensor.""" + + entity_description: AirOSBinarySensorEntityDescription + + def __init__( + self, + coordinator: AirOSDataUpdateCoordinator, + description: AirOSBinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}" + + @property + def is_on(self) -> bool: + """Return the state of the binary sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index 758902bbaa2260..b9bd2db1ae4781 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -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.6"] } diff --git a/homeassistant/components/airos/quality_scale.yaml b/homeassistant/components/airos/quality_scale.yaml index c8c5d209af5c34..e8a5ce8ed894f9 100644 --- a/homeassistant/components/airos/quality_scale.yaml +++ b/homeassistant/components/airos/quality_scale.yaml @@ -54,9 +54,7 @@ rules: dynamic-devices: todo entity-category: done entity-device-class: done - entity-disabled-by-default: - status: todo - comment: prepared binary_sensors will provide this + entity-disabled-by-default: done entity-translations: done exception-translations: done icon-translations: diff --git a/homeassistant/components/airos/strings.json b/homeassistant/components/airos/strings.json index ff013862ee56d1..b11a87d7f75196 100644 --- a/homeassistant/components/airos/strings.json +++ b/homeassistant/components/airos/strings.json @@ -26,6 +26,23 @@ } }, "entity": { + "binary_sensor": { + "port_forwarding": { + "name": "Port forwarding" + }, + "dhcp_client": { + "name": "DHCP client" + }, + "dhcp_server": { + "name": "DHCP server" + }, + "dhcp6_server": { + "name": "DHCPv6 server" + }, + "pppoe": { + "name": "PPPoE link" + } + }, "sensor": { "host_cpuload": { "name": "CPU load" diff --git a/homeassistant/components/anthropic/const.py b/homeassistant/components/anthropic/const.py index a1637a8cef62b1..356140ff66e20e 100644 --- a/homeassistant/components/anthropic/const.py +++ b/homeassistant/components/anthropic/const.py @@ -20,10 +20,8 @@ MIN_THINKING_BUDGET = 1024 THINKING_MODELS = [ - "claude-3-7-sonnet-20250219", - "claude-3-7-sonnet-latest", - "claude-opus-4-20250514", - "claude-opus-4-0", - "claude-sonnet-4-20250514", + "claude-3-7-sonnet", "claude-sonnet-4-0", + "claude-opus-4-0", + "claude-opus-4-1", ] diff --git a/homeassistant/components/anthropic/entity.py b/homeassistant/components/anthropic/entity.py index 636417dd43b6d4..a58130ccd92bac 100644 --- a/homeassistant/components/anthropic/entity.py +++ b/homeassistant/components/anthropic/entity.py @@ -361,7 +361,10 @@ async def _async_handle_chat_log( "system": system.content, "stream": True, } - if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET: + if ( + model.startswith(tuple(THINKING_MODELS)) + and thinking_budget >= MIN_THINKING_BUDGET + ): model_args["thinking"] = ThinkingConfigEnabledParam( type="enabled", budget_tokens=thinking_budget ) diff --git a/homeassistant/components/anthropic/manifest.json b/homeassistant/components/anthropic/manifest.json index 6a8f1e5e54cb60..6fed0282a00172 100644 --- a/homeassistant/components/anthropic/manifest.json +++ b/homeassistant/components/anthropic/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/anthropic", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["anthropic==0.52.0"] + "requirements": ["anthropic==0.62.0"] } diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 3713b74fff7b5c..5e5a81c358a7e6 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/apcupsd", "iot_class": "local_polling", "loggers": ["apcaccess"], + "quality_scale": "bronze", "requirements": ["aioapcaccess==0.4.2"] } diff --git a/homeassistant/components/apcupsd/quality_scale.yaml b/homeassistant/components/apcupsd/quality_scale.yaml new file mode 100644 index 00000000000000..18fc15cd614b56 --- /dev/null +++ b/homeassistant/components/apcupsd/quality_scale.yaml @@ -0,0 +1,99 @@ +rules: + # Bronze + action-setup: done + appropriate-polling: done + brands: done + common-modules: + status: done + comment: | + Consider deriving a base entity. + config-flow-test-coverage: + status: done + comment: | + Consider looking into making a `mock_setup_entry` fixture that just automatically do this. + `test_config_flow_cannot_connect`: Needs to end in CREATE_ENTRY to test that its able to recover. + `test_config_flow_duplicate`: this test should be split in 2, one for testing duplicate host/port and one for duplicate serial number. + `test_flow_works`: Should also test unique id. + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: | + The integration does not provide any actions. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: | + Entities of this integration does not explicitly subscribe to events. + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + # Silver + action-exceptions: + status: exempt + comment: | + The integration does not provide any actions. + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: | + The integration does not provide any additional options. + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: + status: exempt + comment: | + The integration does not require authentication. + test-coverage: + status: todo + comment: | + Patch `aioapcaccess.request_status` where we use it. + # Gold + devices: done + diagnostics: done + discovery-update-info: + status: exempt + comment: | + This integration cannot be discovered. + discovery: + status: exempt + comment: | + This integration cannot be discovered. + docs-data-update: done + docs-examples: done + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: done + docs-troubleshooting: done + docs-use-cases: done + dynamic-devices: + status: exempt + comment: | + The integration connects to a single service per configuration entry. + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: done + icon-translations: done + reconfiguration-flow: done + repair-issues: done + stale-devices: + status: exempt + comment: | + This integration connect to a single service per configuration entry. + # Platinum + async-dependency: done + inject-websession: + status: exempt + comment: | + The integration does not connect via HTTP. + strict-typing: done diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 10f7cb115dab6e..a7bb8a0c550744 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging +API_ABS_HUMID = "abs_humid" API_CO2 = "carbon_dioxide" API_DEW_POINT = "dew_point" API_DUST = "dust" diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index d1f3ec34bf47ed..b0a44cb3e17960 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ATTR_CONNECTIONS, ATTR_SW_VERSION, + CONCENTRATION_GRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, @@ -33,6 +34,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( + API_ABS_HUMID, API_CO2, API_DEW_POINT, API_DUST, @@ -120,6 +122,14 @@ class AwairSensorEntityDescription(SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + AwairSensorEntityDescription( + key=API_ABS_HUMID, + device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY, + native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER, + unique_id_tag="absolute_humidity", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), ) SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = ( diff --git a/homeassistant/components/emoncms/strings.json b/homeassistant/components/emoncms/strings.json index f68ea92d26ccff..e41a7e8bd0336a 100644 --- a/homeassistant/components/emoncms/strings.json +++ b/homeassistant/components/emoncms/strings.json @@ -111,14 +111,6 @@ } }, "issues": { - "remove_value_template": { - "title": "The {domain} integration cannot start", - "description": "Configuring {domain} using YAML is being removed and the `{parameter}` parameter cannot be imported.\n\nPlease remove `{parameter}` from your `{domain}` yaml configuration and restart Home Assistant\n\nAlternatively, you may entirely remove the `{domain}` configuration from your configuration.yaml, restart Home Assistant, and add the {domain} integration manually." - }, - "missing_include_only_feed_id": { - "title": "No feed synchronized with the {domain} sensor", - "description": "Configuring {domain} using YAML is being removed.\n\nPlease add manually the feeds you want to synchronize with the `configure` button of the integration." - }, "migrate_database": { "title": "Upgrade your emoncms version", "description": "Your [emoncms]({url}) does not ship a unique identifier.\n\nPlease upgrade to at least version 11.5.7 and migrate your emoncms database.\n\nMore info in the [emoncms documentation]({doc_url})" diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index a037df474cc362..dc35c47ff4a861 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -12,6 +12,7 @@ ApiError, AuthError, HusqvarnaTimeoutError, + HusqvarnaWSClientError, HusqvarnaWSServerHandshakeError, ) from aioautomower.model import MowerDictionary, MowerStates @@ -172,7 +173,7 @@ async def client_listen( # 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, diff --git a/homeassistant/components/launch_library/strings.json b/homeassistant/components/launch_library/strings.json index a587544f836b63..219d71600bcedc 100644 --- a/homeassistant/components/launch_library/strings.json +++ b/homeassistant/components/launch_library/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Do you want to configure the Launch Library?" + "description": "Do you want to configure Launch Library?" } } }, diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index c45c2b997b3f16..0b2fa75b5c07a0 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -49,6 +49,7 @@ 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 @@ 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 @@ async def async_step_model( model = options[CONF_CHAT_MODEL] - if model.startswith("o"): + if model.startswith(("o", "gpt-5")): step_schema.update( { vol.Optional( @@ -331,7 +333,9 @@ async def async_step_model( 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 @@ async def async_step_model( 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) ): diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index cacef6fcff92ba..2fd189132074f1 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -21,6 +21,7 @@ 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_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 diff --git a/homeassistant/components/openai_conversation/entity.py b/homeassistant/components/openai_conversation/entity.py index c1b2f970f07f8f..748c0c8f87441d 100644 --- a/homeassistant/components/openai_conversation/entity.py +++ b/homeassistant/components/openai_conversation/entity.py @@ -61,6 +61,7 @@ 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 @@ RECOMMENDED_REASONING_EFFORT, RECOMMENDED_TEMPERATURE, RECOMMENDED_TOP_P, + RECOMMENDED_VERBOSITY, RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE, ) @@ -346,14 +348,18 @@ async def _async_handle_chat_log( 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 diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index a1bf236f19b7f8..304ef8b6bdcc8e 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -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": { diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py index 8818eff2d81255..826d5872d7cdac 100644 --- a/homeassistant/components/progettihwsw/config_flow.py +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -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"], } diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 76e0a915060a50..440d9a3aea7ffc 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -186,6 +186,9 @@ "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 diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index f5cfb84ec36fbf..894d32fcb97e14 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -35,6 +35,7 @@ from .alarms import SonosAlarms from .const import ( + ATTR_SPEECH_ENHANCEMENT_ENABLED, AVAILABILITY_TIMEOUT, BATTERY_SCAN_INTERVAL, DOMAIN, @@ -157,6 +158,7 @@ def __init__( # 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 @@ def async_dispatch_media_update(self, event: SonosEvent) -> None: @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 @@ def async_update_volume(self, event: SonosEvent) -> None: for bool_var in ( "dialog_level", + ATTR_SPEECH_ENHANCEMENT_ENABLED, "night_mode", "sub_enabled", "surround_enabled", diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 582845d10a2122..653be229b22d90 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -19,7 +19,9 @@ 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 @@ ATTR_SURROUND_ENABLED, ATTR_STATUS_LIGHT, ) +ALL_SUBST_FEATURES = (ATTR_SPEECH_ENHANCEMENT_ENABLED,) COORDINATOR_FEATURES = ATTR_CROSSFADE @@ -69,6 +72,14 @@ 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_create_alarms(speaker: SonosSpeaker, alarm_ids: list[str]) -> N 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_create_switches(speaker: SonosSpeaker) -> None: 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 @@ async def _async_fallback_poll(self) -> None: @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 @@ def send_command(self, enable: bool) -> None: 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) diff --git a/homeassistant/components/switchbot/icons.json b/homeassistant/components/switchbot/icons.json index 2aef019aab4a21..cf9217bf70b261 100644 --- a/homeassistant/components/switchbot/icons.json +++ b/homeassistant/components/switchbot/icons.json @@ -1,6 +1,21 @@ { "entity": { "sensor": { + "light_level": { + "default": "mdi:brightness-7", + "state": { + "1": "mdi:brightness-1", + "2": "mdi:brightness-1", + "3": "mdi:brightness-2", + "4": "mdi:brightness-3", + "5": "mdi:brightness-4", + "6": "mdi:brightness-5", + "7": "mdi:brightness-5", + "8": "mdi:brightness-6", + "9": "mdi:brightness-6", + "10": "mdi:brightness-7" + } + }, "water_level": { "default": "mdi:water-percent", "state": { diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index f6c5d526ab78dc..9196453e98c8f5 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -67,7 +67,6 @@ "lightLevel": SensorEntityDescription( key="lightLevel", translation_key="light_level", - native_unit_of_measurement="Level", state_class=SensorStateClass.MEASUREMENT, ), "humidity": SensorEntityDescription( diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index d660c9c910dcb3..16e7e555485d50 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -472,7 +472,7 @@ }, "blanket_level": { "state": { - "level_1": "Low", + "level_1": "[%key:common::state::low%]", "level_2": "Level 2", "level_3": "Level 3", "level_4": "Level 4", @@ -481,7 +481,7 @@ "level_7": "Level 7", "level_8": "Level 8", "level_9": "Level 9", - "level_10": "High" + "level_10": "[%key:common::state::high%]" } }, "odor_elimination_mode": { diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 708a4883dddae6..5a3dcc6ddfddb6 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -60,43 +60,31 @@ SERVICE_GET_USER_KEYRING_INFO, ] -DOORBELL_TEXT_SCHEMA = vol.All( - vol.Schema( - { - **cv.ENTITY_SERVICE_FIELDS, - vol.Required(ATTR_MESSAGE): cv.string, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID), +DOORBELL_TEXT_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_MESSAGE): cv.string, + }, ) -CHIME_PAIRED_SCHEMA = vol.All( - vol.Schema( - { - **cv.ENTITY_SERVICE_FIELDS, - "doorbells": cv.TARGET_SERVICE_FIELDS, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID), +CHIME_PAIRED_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + "doorbells": cv.ENTITY_SERVICE_FIELDS, + }, ) -REMOVE_PRIVACY_ZONE_SCHEMA = vol.All( - vol.Schema( - { - **cv.ENTITY_SERVICE_FIELDS, - vol.Required(ATTR_NAME): cv.string, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID), +REMOVE_PRIVACY_ZONE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_NAME): cv.string, + }, ) -GET_USER_KEYRING_INFO_SCHEMA = vol.All( - vol.Schema( - { - **cv.ENTITY_SERVICE_FIELDS, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID), +GET_USER_KEYRING_INFO_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + }, ) diff --git a/homeassistant/components/uptime_kuma/coordinator.py b/homeassistant/components/uptime_kuma/coordinator.py index 58eed420fd8280..df64b12f8e9bba 100644 --- a/homeassistant/components/uptime_kuma/coordinator.py +++ b/homeassistant/components/uptime_kuma/coordinator.py @@ -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, diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index 647c7b578e88a7..caadebb6e2a242 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -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 @@ class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription): """Describes a Volvo sensor entity.""" - source_fields: list[str] | None = None value_fn: Callable[[VolvoCarsValue], Any] | None = None @@ -240,11 +239,15 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: "none", ], ), - # statistics & energy state endpoint + # statistics endpoint + # We're not using `electricRange` from the energy state endpoint because + # the official app seems to use `distanceToEmptyBattery`. + # In issue #150213, a user described to behavior as follows: + # - For a `distanceToEmptyBattery` of 250km, the `electricRange` was 150mi + # - For a `distanceToEmptyBattery` of 260km, the `electricRange` was 160mi VolvoSensorDescription( 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, @@ -362,12 +365,7 @@ def _add_entity( 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) diff --git a/requirements_all.txt b/requirements_all.txt index 51b4a5d34e4674..2cdb87b5ad2c1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.4 +airos==0.2.6 # homeassistant.components.airthings_ble airthings-ble==0.9.2 @@ -495,7 +495,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.52.0 +anthropic==0.62.0 # homeassistant.components.mcp_server anyio==4.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac747cfdaa2c9d..9dfd138977c221 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.4 +airos==0.2.6 # homeassistant.components.airthings_ble airthings-ble==0.9.2 @@ -468,7 +468,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.52.0 +anthropic==0.62.0 # homeassistant.components.mcp_server anyio==4.9.0 diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 1d6db8e1f7a384..0050cddc708a05 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -160,7 +160,6 @@ class Rule: "anthropic", "aosmith", "apache_kafka", - "apcupsd", "apple_tv", "apprise", "aprilaire", @@ -1191,7 +1190,6 @@ class Rule: "anthropic", "aosmith", "apache_kafka", - "apcupsd", "apple_tv", "apprise", "aprilaire", diff --git a/tests/components/airos/__init__.py b/tests/components/airos/__init__.py index 8c6182a86506f3..f663644a8a4418 100644 --- a/tests/components/airos/__init__.py +++ b/tests/components/airos/__init__.py @@ -1,13 +1,19 @@ """Tests for the Ubiquity airOS integration.""" +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, patch -async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: +async def setup_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + platforms: list[Platform] | None = None, +) -> None: """Fixture for setting up the component.""" - config_entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patch("homeassistant.components.airos._PLATFORMS", platforms): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/airos/conftest.py b/tests/components/airos/conftest.py index b17908e801aadd..5443f79a976308 100644 --- a/tests/components/airos/conftest.py +++ b/tests/components/airos/conftest.py @@ -15,7 +15,7 @@ @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) diff --git a/tests/components/airos/fixtures/airos_ap-ptp.json b/tests/components/airos/fixtures/airos_loco5ac_ap-ptp.json similarity index 80% rename from tests/components/airos/fixtures/airos_ap-ptp.json rename to tests/components/airos/fixtures/airos_loco5ac_ap-ptp.json index 06d13ba11015b6..a033a82411c891 100644 --- a/tests/components/airos/fixtures/airos_ap-ptp.json +++ b/tests/components/airos/fixtures/airos_loco5ac_ap-ptp.json @@ -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 + } ], + "derived": { + "access_point": true, + "mac": "01:23:45:67:89:AB", + "mac_interface": "br0", + "ptmp": false, + "ptp": true, + "station": false + }, + "firewall": { + "eb6tables": false, + "ebtables": false, + "ip6tables": false, + "iptables": false + }, + "genuine": "/images/genuine.png", + "gps": { + "fix": 0, + "lat": 52.379894, + "lon": 4.901608 + }, "host": { - "hostname": "NanoStation 5AC ap name", + "cpuload": 10.10101, "device_id": "03aa0d0b40fed0a47088293584ef5432", - "uptime": 264888, + "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, - "fwversion": "v8.7.17", - "devmodel": "NanoStation 5AC loco", - "netrole": "bridge", - "loadavg": 0.412598, "totalram": 63447040, - "freeram": 16564224, - "temperature": 0, - "cpuload": 10.10101, - "height": 3 + "uptime": 264888 }, - "genuine": "/images/genuine.png", + "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, - "dhcp6d_stateful": false, - "pppoe": false, - "airview": 2 + "pppoe": false }, - "firewall": { - "iptables": false, - "ebtables": false, - "ip6tables": false, - "eb6tables": false + "unms": { + "status": 0, + "timestamp": null }, - "portfw": false, "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, + "apmac": "01:23:45:67:89:AB", + "aprepeater": false, + "band": 2, + "cac_state": 0, + "cac_timeout": 0, "center1_freq": 5530, + "chanbw": 80, + "compat_11n": 0, + "count": 1, "dfs": 1, "distance": 0, - "security": "WPA2", + "essid": "DemoSSID", + "frequency": 5500, + "hide_essid": 0, + "ieeemode": "11ACVHT80", + "mode": "ap-ptp", "noisef": -89, - "txpower": -3, - "aprepeater": false, - "rstatus": 5, - "chanbw": 80, - "rx_chainmask": 3, - "tx_chainmask": 3, "nol_state": 0, "nol_timeout": 0, - "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 }, "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, + "atpc_status": 2, "beam": 0, - "desired_priority": 0, "cb_capacity": 593970, + "desired_priority": 0, "dl_capacity": 647400, - "ul_capacity": 540540, - "atpc_status": 2, "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, - "chainrssi": [33, 37, 0], - "tx_ratedata": [ - 14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154 - ], - "tx_bytes": 212308148210, - "rx_bytes": 3624206478, + "airview": 2, "antenna_gain": 13, "cable_loss": 0, - "height": 2, + "chainrssi": [33, 37, 0], + "compat_11n": 0, + "cpuload": 43.564301, + "device_id": "d4f4cdf82961e619328a8f72f8d7653b", + "distance": 1, "ethlist": [ { - "ifname": "eth0", + "cable_len": 14, + "duplex": true, "enabled": true, + "ifname": "eth0", "plugged": true, - "duplex": true, - "speed": 1000, "snr": [30, 30, 29, 30], - "cable_len": 14 + "speed": 1000 } ], - "ipaddr": ["192.168.1.2"], + "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"], - "gps": { "lat": "52.379894", "lon": "4.901608", "fix": 0 }, + "ipaddr": ["192.168.1.2"], + "mode": "sta-ptp", + "netrole": "bridge", + "noisefloor": -90, "oob": false, - "unms": { "status": 0, "timestamp": null }, - "airview": 2, - "service": { "time": 267195, "link": 265996 } + "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_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 + } } diff --git a/tests/components/airos/snapshots/test_binary_sensor.ambr b/tests/components/airos/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000000..d9815e0c62bd15 --- /dev/null +++ b/tests/components/airos/snapshots/test_binary_sensor.ambr @@ -0,0 +1,245 @@ +# serializer version: 1 +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcp_client-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcp_client', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHCP client', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dhcp_client', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_dhcp_client', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcp_client-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'NanoStation 5AC ap name DHCP client', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcp_client', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcp_server-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcp_server', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHCP server', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dhcp_server', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_dhcp_server', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcp_server-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'NanoStation 5AC ap name DHCP server', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcp_server', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcpv6_server-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcpv6_server', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHCPv6 server', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dhcp6_server', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_dhcp6_server', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcpv6_server-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'NanoStation 5AC ap name DHCPv6 server', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcpv6_server', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_port_forwarding-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_port_forwarding', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Port forwarding', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'port_forwarding', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_portfw', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_port_forwarding-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NanoStation 5AC ap name Port forwarding', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_port_forwarding', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_pppoe_link-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_pppoe_link', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PPPoE link', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pppoe', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_pppoe', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_pppoe_link-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'NanoStation 5AC ap name PPPoE link', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_pppoe_link', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/airos/snapshots/test_diagnostics.ambr b/tests/components/airos/snapshots/test_diagnostics.ambr index bc2dedc905aa63..574dbf689497cc 100644 --- a/tests/components/airos/snapshots/test_diagnostics.ambr +++ b/tests/components/airos/snapshots/test_diagnostics.ambr @@ -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**', diff --git a/tests/components/airos/test_binary_sensor.py b/tests/components/airos/test_binary_sensor.py new file mode 100644 index 00000000000000..40c3d631cd3e17 --- /dev/null +++ b/tests/components/airos/test_binary_sensor.py @@ -0,0 +1,28 @@ +"""Test the Ubiquiti airOS binary sensors.""" + +from unittest.mock import AsyncMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + await setup_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR]) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/airos/test_sensor.py b/tests/components/airos/test_sensor.py index c9e675e7987149..7f39f504753688 100644 --- a/tests/components/airos/test_sensor.py +++ b/tests/components/airos/test_sensor.py @@ -13,7 +13,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.airos.const import SCAN_INTERVAL -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -31,7 +31,7 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @@ -53,7 +53,7 @@ async def test_sensor_update_exception_handling( freezer: FrozenDateTimeFactory, ) -> None: """Test entity update data handles exceptions.""" - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) expected_entity_id = "sensor.nanostation_5ac_ap_name_antenna_gain" signal_state = hass.states.get(expected_entity_id) @@ -65,7 +65,7 @@ async def test_sensor_update_exception_handling( mock_airos_client.login.side_effect = exception - freezer.tick(timedelta(seconds=SCAN_INTERVAL.total_seconds() + 1)) + freezer.tick(timedelta(seconds=SCAN_INTERVAL.total_seconds())) async_fire_time_changed(hass) await hass.async_block_till_done() diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py index b58e6c31f381b8..38d8967e6c53b5 100644 --- a/tests/components/openai_conversation/conftest.py +++ b/tests/components/openai_conversation/conftest.py @@ -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 diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index 6d8fb143f88c96..3f3b7801c8f349 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -20,6 +20,7 @@ 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", }, ( { diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 8dcc69173467c8..c41c88ec950870 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -12,9 +12,9 @@ mock_value_step_user = { "title": "1R & 1IN Board", - "relay_count": 1, - "input_count": 1, - "is_old": False, + "relays": 1, + "inputs": 1, + "temps": False, } diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index d3de2a889d51cb..0cdc17c55a6299 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -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) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 04457ee95c7735..c7df2062b0f31e 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -6,13 +6,18 @@ 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.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"), [ diff --git a/tests/components/switchbot/test_sensor.py b/tests/components/switchbot/test_sensor.py index 411d728289350a..645eb5d1ab31c1 100644 --- a/tests/components/switchbot/test_sensor.py +++ b/tests/components/switchbot/test_sensor.py @@ -320,13 +320,12 @@ async def test_hub2_sensor(hass: HomeAssistant) -> None: light_level_sensor_attrs = light_level_sensor.attributes assert light_level_sensor.state == "4" assert light_level_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Light level" - assert light_level_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "Level" - light_level_sensor = hass.states.get("sensor.test_name_illuminance") - light_level_sensor_attrs = light_level_sensor.attributes - assert light_level_sensor.state == "30" - assert light_level_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Illuminance" - assert light_level_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "lx" + illuminance_sensor = hass.states.get("sensor.test_name_illuminance") + illuminance_sensor_attrs = illuminance_sensor.attributes + assert illuminance_sensor.state == "30" + assert illuminance_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Illuminance" + assert illuminance_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "lx" rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal") rssi_sensor_attrs = rssi_sensor.attributes @@ -474,7 +473,6 @@ async def test_hub3_sensor(hass: HomeAssistant) -> None: light_level_sensor_attrs = light_level_sensor.attributes assert light_level_sensor.state == "3" assert light_level_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Light level" - assert light_level_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "Level" assert light_level_sensor_attrs[ATTR_STATE_CLASS] == "measurement" illuminance_sensor = hass.states.get("sensor.test_name_illuminance") diff --git a/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json b/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json index 16208571c4783b..bac596857b0151 100644 --- a/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json +++ b/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json @@ -7,8 +7,8 @@ }, "electricRange": { "status": "OK", - "value": 220, - "unit": "km", + "value": 150, + "unit": "mi", "updatedAt": "2025-07-02T08:51:23Z" }, "chargerConnectionStatus": { diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index b651bbd526f1e1..53e05c49c970bf 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -585,7 +585,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '327', + 'state': '250', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_service-entry] @@ -2514,7 +2514,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '220', + 'state': '250', }) # --- # name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_distance_to_service-entry] diff --git a/tests/components/volvo/test_sensor.py b/tests/components/volvo/test_sensor.py index f610ee2ed5731d..e4cc69470aeada 100644 --- a/tests/components/volvo/test_sensor.py +++ b/tests/components/volvo/test_sensor.py @@ -30,3 +30,19 @@ 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"