diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 2bc2d5e726afe6..6abe3e5d174a4f 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -105,6 +105,7 @@ ), "illuminance": SensorEntityDescription( key="illuminance", + translation_key="illuminance", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/airthings_ble/strings.json b/homeassistant/components/airthings_ble/strings.json index b7343377a2bad0..6f17b9a317e609 100644 --- a/homeassistant/components/airthings_ble/strings.json +++ b/homeassistant/components/airthings_ble/strings.json @@ -33,6 +33,9 @@ }, "radon_longterm_level": { "name": "Radon longterm level" + }, + "illuminance": { + "name": "[%key:component::sensor::entity_component::illuminance::name%]" } } } diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index f56df16918ef25..44d615bf5346b9 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==54"], + "requirements": ["axis==57"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 55d47bb0c2c063..3c60ccba5f0524 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -58,7 +58,7 @@ async def async_step_user( return self.async_create_entry(title=title, data=user_input) except InvalidHost: errors[CONF_HOST] = "wrong_host" - except ConnectionError: + except (ConnectionError, TimeoutError): errors["base"] = "cannot_connect" except SnmpError: errors["base"] = "snmp_error" @@ -88,7 +88,7 @@ async def async_step_zeroconf( await self.brother.async_update() except UnsupportedModelError: return self.async_abort(reason="unsupported_model") - except (ConnectionError, SnmpError): + except (ConnectionError, SnmpError, TimeoutError): return self.async_abort(reason="cannot_connect") # Check if already configured diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 8c574e0792bacc..9aed870d9b41ce 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -24,9 +24,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cast from a config entry.""" + hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await home_assistant_cast.async_setup_ha_cast(hass, entry) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform) return True diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index d02bcd3558a455..1d06ae23ca2b93 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "iot_class": "local_polling", "loggers": ["casttube", "pychromecast"], - "requirements": ["PyChromecast==14.0.0"], + "requirements": ["PyChromecast==14.0.1"], "zeroconf": ["_googlecast._tcp.local."] } diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b2893a5431018f..5e907b0a659833 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -4,9 +4,10 @@ from collections.abc import Callable from contextlib import suppress from datetime import datetime +from functools import wraps import json import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -18,6 +19,7 @@ ) from pychromecast.controllers.multizone import MultizoneManager from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED +from pychromecast.error import PyChromecastError from pychromecast.quick_play import quick_play from pychromecast.socket_client import ( CONNECTION_STATUS_CONNECTED, @@ -83,6 +85,34 @@ CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" +_CastDeviceT = TypeVar("_CastDeviceT", bound="CastDevice") +_R = TypeVar("_R") +_P = ParamSpec("_P") + +_FuncType = Callable[Concatenate[_CastDeviceT, _P], _R] +_ReturnFuncType = Callable[Concatenate[_CastDeviceT, _P], _R] + + +def api_error( + func: _FuncType[_CastDeviceT, _P, _R], +) -> _ReturnFuncType[_CastDeviceT, _P, _R]: + """Handle PyChromecastError and reraise a HomeAssistantError.""" + + @wraps(func) + def wrapper(self: _CastDeviceT, *args: _P.args, **kwargs: _P.kwargs) -> _R: + """Wrap a CastDevice method.""" + try: + return_value = func(self, *args, **kwargs) + except PyChromecastError as err: + raise HomeAssistantError( + f"{self.__class__.__name__}.{func.__name__} Failed: {err}" + ) from err + + return return_value + + return wrapper + + @callback def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo): """Create a CastDevice entity or dynamic group from the chromecast object. @@ -476,6 +506,21 @@ def _media_controller(self): return media_controller + @api_error + def _quick_play(self, app_name: str, data: dict[str, Any]) -> None: + """Launch the app `app_name` and start playing media defined by `data`.""" + quick_play(self._get_chromecast(), app_name, data) + + @api_error + def _quit_app(self) -> None: + """Quit the currently running app.""" + self._get_chromecast().quit_app() + + @api_error + def _start_app(self, app_id: str) -> None: + """Start an app.""" + self._get_chromecast().start_app(app_id) + def turn_on(self) -> None: """Turn on the cast device.""" @@ -486,52 +531,61 @@ def turn_on(self) -> None: if chromecast.app_id is not None: # Quit the previous app before starting splash screen or media player - chromecast.quit_app() + self._quit_app() # The only way we can turn the Chromecast is on is by launching an app if chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"} - quick_play(chromecast, "default_media_receiver", app_data) + self._quick_play("default_media_receiver", app_data) else: - chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) + self._start_app(pychromecast.config.APP_MEDIA_RECEIVER) + @api_error def turn_off(self) -> None: """Turn off the cast device.""" self._get_chromecast().quit_app() + @api_error def mute_volume(self, mute: bool) -> None: """Mute the volume.""" self._get_chromecast().set_volume_muted(mute) + @api_error def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._get_chromecast().set_volume(volume) + @api_error def media_play(self) -> None: """Send play command.""" media_controller = self._media_controller() media_controller.play() + @api_error def media_pause(self) -> None: """Send pause command.""" media_controller = self._media_controller() media_controller.pause() + @api_error def media_stop(self) -> None: """Send stop command.""" media_controller = self._media_controller() media_controller.stop() + @api_error def media_previous_track(self) -> None: """Send previous track command.""" media_controller = self._media_controller() media_controller.queue_prev() + @api_error def media_next_track(self) -> None: """Send next track command.""" media_controller = self._media_controller() media_controller.queue_next() + @api_error def media_seek(self, position: float) -> None: """Seek the media to a specific location.""" media_controller = self._media_controller() @@ -644,7 +698,7 @@ async def async_play_media( if "app_id" in app_data: app_id = app_data.pop("app_id") _LOGGER.info("Starting Cast app by ID %s", app_id) - await self.hass.async_add_executor_job(chromecast.start_app, app_id) + await self.hass.async_add_executor_job(self._start_app, app_id) if app_data: _LOGGER.warning( "Extra keys %s were ignored. Please use app_name to cast media", @@ -655,7 +709,7 @@ async def async_play_media( app_name = app_data.pop("app_name") try: await self.hass.async_add_executor_job( - quick_play, chromecast, app_name, app_data + self._quick_play, app_name, app_data ) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) @@ -729,7 +783,7 @@ async def async_play_media( app_data, ) await self.hass.async_add_executor_job( - quick_play, chromecast, "default_media_receiver", app_data + self._quick_play, "default_media_receiver", app_data ) def _media_status(self): diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 95c925a8d33200..673a80b8d9fe1f 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==0.8.1", + "aiodhcpwatcher==0.8.2", "aiodiscover==1.6.1", "cached_ipaddress==0.3.0" ] diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index a08daee89618ba..ec9fb7018d6708 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.1"] + "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.3"] } diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index f00baad661de3e..ed013f2e0c25b8 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.3.0"] + "requirements": ["aioautomower==2024.3.3"] } diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 970c444737c340..31eebde9c81143 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -69,6 +69,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_charging_time is not None, value_fn=lambda data: data.statistics.total_charging_time, ), AutomowerSensorEntityDescription( @@ -79,6 +80,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_cutting_time is not None, value_fn=lambda data: data.statistics.total_cutting_time, ), AutomowerSensorEntityDescription( @@ -89,6 +91,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_running_time is not None, value_fn=lambda data: data.statistics.total_running_time, ), AutomowerSensorEntityDescription( @@ -99,6 +102,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_searching_time is not None, value_fn=lambda data: data.statistics.total_searching_time, ), AutomowerSensorEntityDescription( @@ -107,6 +111,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription): icon="mdi:battery-sync-outline", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL, + exists_fn=lambda data: data.statistics.number_of_charging_cycles is not None, value_fn=lambda data: data.statistics.number_of_charging_cycles, ), AutomowerSensorEntityDescription( @@ -115,6 +120,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription): icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL, + exists_fn=lambda data: data.statistics.number_of_collisions is not None, value_fn=lambda data: data.statistics.number_of_collisions, ), AutomowerSensorEntityDescription( @@ -125,6 +131,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=UnitOfLength.METERS, suggested_unit_of_measurement=UnitOfLength.KILOMETERS, + exists_fn=lambda data: data.statistics.total_drive_distance is not None, value_fn=lambda data: data.statistics.total_drive_distance, ), AutomowerSensorEntityDescription( diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 25ec9f2ccc6d96..1c13970503d2a1 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==7.0.1"] + "requirements": ["ical==7.0.3"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 81f0f9dc199fb3..3bcb8af9f4384c 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==7.0.1"] + "requirements": ["ical==7.0.3"] } diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 6b072457144b8c..14faad789fec90 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "gold", - "requirements": ["pymodbus==3.6.5"] + "requirements": ["pymodbus==3.6.6"] } diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 33a749909282cb..6d2b6c48e1b02c 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -165,9 +165,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except NotionError as err: raise ConfigEntryNotReady("Config entry failed to load") from err - # Always update the config entry with the latest refresh token and user UUID: - entry_updates["data"][CONF_REFRESH_TOKEN] = client.refresh_token - entry_updates["data"][CONF_USER_UUID] = client.user_uuid + # Update the Notion user UUID and refresh token if they've changed: + for key, value in ( + (CONF_REFRESH_TOKEN, client.refresh_token), + (CONF_USER_UUID, client.user_uuid), + ): + if entry.data[key] == value: + continue + entry_updates["data"][key] = value + + hass.config_entries.async_update_entry(entry, **entry_updates) @callback def async_save_refresh_token(refresh_token: str) -> None: @@ -180,12 +187,6 @@ def async_save_refresh_token(refresh_token: str) -> None: # Create a callback to save the refresh token when it changes: entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token)) - # Save the client's refresh token if it's different than what we already have: - if (token := client.refresh_token) and token != entry.data[CONF_REFRESH_TOKEN]: - async_save_refresh_token(token) - - hass.config_entries.async_update_entry(entry, **entry_updates) - async def async_update() -> NotionData: """Get the latest data from the Notion API.""" data = NotionData(hass=hass, entry=entry) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index e7e30588f8a086..5cd3fa65a60231 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -143,6 +143,8 @@ class OneWireBinarySensor(OneWireEntity, BinarySensorEntity): entity_description: OneWireBinarySensorEntityDescription @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if sensor is on.""" + if self._state is None: + return None return bool(self._state) diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 00a3f8f65f436e..c63198ccf05fd8 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -204,8 +204,10 @@ class OneWireSwitch(OneWireEntity, SwitchEntity): entity_description: OneWireSwitchEntityDescription @property - def is_on(self) -> bool: - """Return true if sensor is on.""" + def is_on(self) -> bool | None: + """Return true if switch is on.""" + if self._state is None: + return None return bool(self._state) def turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/rainforest_raven/manifest.json b/homeassistant/components/rainforest_raven/manifest.json index 3e463af9ba4c36..ad161d322010b3 100644 --- a/homeassistant/components/rainforest_raven/manifest.json +++ b/homeassistant/components/rainforest_raven/manifest.json @@ -6,7 +6,7 @@ "dependencies": ["usb"], "documentation": "https://www.home-assistant.io/integrations/rainforest_raven", "iot_class": "local_polling", - "requirements": ["aioraven==0.5.1"], + "requirements": ["aioraven==0.5.2"], "usb": [ { "vid": "0403", diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index d1e1c4f430c4b6..efc17c48a063f8 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -37,12 +37,10 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - CONF_COMMUNICATION_DELAY, DATA_COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENTS_COORDINATOR, - MAX_COMMUNICATION_DELAY, TYPE_LOCAL, ) @@ -85,31 +83,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = entry.data - comm_delay = initial_delay = data.get(CONF_COMMUNICATION_DELAY, 0) - - while True: - risco = RiscoLocal( - data[CONF_HOST], - data[CONF_PORT], - data[CONF_PIN], - communication_delay=comm_delay, - ) - try: - await risco.connect() - except CannotConnectError as error: - if comm_delay >= MAX_COMMUNICATION_DELAY: - raise ConfigEntryNotReady() from error - comm_delay += 1 - except UnauthorizedError: - _LOGGER.exception("Failed to login to Risco cloud") - return False - else: - break - - if comm_delay > initial_delay: - new_data = data.copy() - new_data[CONF_COMMUNICATION_DELAY] = comm_delay - hass.config_entries.async_update_entry(entry, data=new_data) + risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN]) + + try: + await risco.connect() + except CannotConnectError as error: + raise ConfigEntryNotReady() from error + except UnauthorizedError: + _LOGGER.exception("Failed to login to Risco cloud") + return False async def _error(error: Exception) -> None: _LOGGER.error("Error in Risco library: %s", error) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index ca28af3d8e55c5..b5d8c4442fd0cc 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyrisco"], "quality_scale": "platinum", - "requirements": ["pyrisco==0.5.8"] + "requirements": ["pyrisco==0.5.10"] } diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 4afe66199f0605..d41282b1f0b200 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -216,7 +216,7 @@ def _async_device_updates_handler(self) -> None: # Check for input events and config change cfg_changed = 0 for block in self.device.blocks: - if block.type == "device": + if block.type == "device" and block.cfgChanged is not None: cfg_changed = block.cfgChanged # Shelly TRV sends information about changing the configuration for no diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index b88b6886b8480a..82fc4fe6d78548 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -941,6 +941,7 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, + removal_condition=lambda _config, status, key: (status[key]["battery"] is None), ), "voltmeter": RpcSensorDescription( key="voltmeter", diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index cd6c1dd91524c2..c4aa82f2a74570 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/snmp", "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"], - "requirements": ["pysnmp-lextudio==6.0.9"] + "requirements": ["pysnmp-lextudio==6.0.11"] } diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index cdbc041f535d4d..d3ec29ae356fdf 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -2,7 +2,6 @@ import logging from typing import Any -import PyTado import voluptuous as vol from homeassistant.components.water_heater import ( @@ -29,8 +28,6 @@ DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, - TADO_DEFAULT_MAX_TEMP, - TADO_DEFAULT_MIN_TEMP, TYPE_HOT_WATER, ) from .entity import TadoZoneEntity @@ -133,8 +130,8 @@ def __init__( zone_name: str, zone_id: int, supports_temperature_control: bool, - min_temp: float | None = None, - max_temp: float | None = None, + min_temp, + max_temp, ) -> None: """Initialize of Tado water heater entity.""" self._tado = tado @@ -146,8 +143,8 @@ def __init__( self._device_is_active = False self._supports_temperature_control = supports_temperature_control - self._min_temperature = min_temp or TADO_DEFAULT_MIN_TEMP - self._max_temperature = max_temp or TADO_DEFAULT_MAX_TEMP + self._min_temperature = min_temp + self._max_temperature = max_temp self._target_temp: float | None = None @@ -157,7 +154,7 @@ def __init__( self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE - self._tado_zone_data: PyTado.TadoZone = {} + self._tado_zone_data: Any = None async def async_added_to_hass(self) -> None: """Register for sensor updates.""" diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index 1f2a2405a445b8..db3a88f3113bcf 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tedee", "iot_class": "local_push", "loggers": ["pytedee_async"], - "requirements": ["pytedee-async==0.2.16"] + "requirements": ["pytedee-async==0.2.17"] } diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py index 047d58d9208991..a5a38bf7b1dca9 100644 --- a/homeassistant/components/template/coordinator.py +++ b/homeassistant/components/template/coordinator.py @@ -3,7 +3,7 @@ import logging from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import CoreState, callback +from homeassistant.core import Context, CoreState, callback from homeassistant.helpers import discovery, trigger as trigger_helper from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType @@ -90,7 +90,10 @@ async def _attach_triggers(self, start_event=None) -> None: ) async def _handle_triggered_with_script(self, run_variables, context=None): - if script_result := await self._script.async_run(run_variables, context): + # Create a context referring to the trigger context. + trigger_context_id = None if context is None else context.id + script_context = Context(parent_id=trigger_context_id) + if script_result := await self._script.async_run(run_variables, script_context): run_variables = script_result.variables self._handle_triggered(run_variables, context) diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py index 15b94a2b880b42..f4b1cc799cb10e 100644 --- a/homeassistant/components/traccar_server/diagnostics.py +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -12,7 +12,12 @@ from .const import DOMAIN from .coordinator import TraccarServerCoordinator -TO_REDACT = {CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE} +TO_REDACT = { + CONF_ADDRESS, + CONF_LATITUDE, + CONF_LONGITUDE, + "area", # This is the polygon area of a geofence +} async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index c4a6bc880687b0..076095a16b3c3b 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -6,6 +6,7 @@ from aiohttp.client_exceptions import ServerDisconnectedError from pyunifiprotect.data import Bootstrap +from pyunifiprotect.data.types import FirmwareReleaseChannel from pyunifiprotect.exceptions import ClientError, NotAuthorized # Import the test_util.anonymize module from the pyunifiprotect package @@ -111,19 +112,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) ) - if ( - not entry.options.get(CONF_ALLOW_EA, False) - and await nvr_info.get_is_prerelease() + if not entry.options.get(CONF_ALLOW_EA, False) and ( + await nvr_info.get_is_prerelease() + or nvr_info.release_channel != FirmwareReleaseChannel.RELEASE ): ir.async_create_issue( hass, DOMAIN, - "ea_warning", + "ea_channel_warning", is_fixable=True, is_persistent=True, learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", severity=IssueSeverity.WARNING, - translation_key="ea_warning", + translation_key="ea_channel_warning", translation_placeholders={"version": str(nvr_info.version)}, data={"entry_id": entry.entry_id}, ) @@ -149,7 +150,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "version": str(nvr_info.version), }, ) - ir.async_delete_issue(hass, DOMAIN, "ea_warning") + ir.async_delete_issue(hass, DOMAIN, "ea_channel_warning") _LOGGER.exception("Error setting up UniFi Protect integration: %s", err) raise diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 2982ca29c4a0d9..39be5f0e7cb8ea 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -24,7 +24,7 @@ CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" -CONF_ALLOW_EA = "allow_ea" +CONF_ALLOW_EA = "allow_ea_channel" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 2825c2a4f3c4d6..b82e9ff37f16c6 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -19,6 +19,7 @@ WSSubscriptionMessage, ) from pyunifiprotect.exceptions import ClientError, NotAuthorized +from pyunifiprotect.utils import log_event from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -41,11 +42,6 @@ _LOGGER = logging.getLogger(__name__) ProtectDeviceType = ProtectAdoptableDeviceModel | NVR -SMART_EVENTS = { - EventType.SMART_DETECT, - EventType.SMART_AUDIO_DETECT, - EventType.SMART_DETECT_LINE, -} @callback @@ -230,26 +226,7 @@ def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: # trigger updates for camera that the event references elif isinstance(obj, Event): # type: ignore[unreachable] if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("event WS msg: %s", obj.dict()) - if obj.type in SMART_EVENTS: - if obj.camera is not None: - if obj.end is None: - _LOGGER.debug( - "%s (%s): New smart detection started for %s (%s)", - obj.camera.name, - obj.camera.mac, - obj.smart_detect_types, - obj.id, - ) - else: - _LOGGER.debug( - "%s (%s): Smart detection ended for %s (%s)", - obj.camera.name, - obj.camera.mac, - obj.smart_detect_types, - obj.id, - ) - + log_event(obj) if obj.type is EventType.DEVICE_ADOPTED: if obj.metadata is not None and obj.metadata.device_id is not None: device = self.api.bootstrap.get_device_from_id( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index eba2b934e05837..1eb37befca02a5 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -42,7 +42,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.23.3", "unifi-discovery==1.1.8"], + "requirements": ["pyunifiprotect==5.0.2", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py index ddc0a257c146f5..254984da5159df 100644 --- a/homeassistant/components/unifiprotect/repairs.py +++ b/homeassistant/components/unifiprotect/repairs.py @@ -6,6 +6,7 @@ from typing import cast from pyunifiprotect import ProtectApiClient +from pyunifiprotect.data.types import FirmwareReleaseChannel import voluptuous as vol from homeassistant import data_entry_flow @@ -68,7 +69,7 @@ async def async_step_start( ) nvr = await self._api.get_nvr() - if await nvr.get_is_prerelease(): + if nvr.release_channel != FirmwareReleaseChannel.RELEASE: return await self.async_step_confirm() await self.hass.config_entries.async_reload(self._entry.entry_id) return self.async_create_entry(data={}) @@ -124,7 +125,7 @@ async def async_create_fix_flow( data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create flow.""" - if data is not None and issue_id == "ea_warning": + if data is not None and issue_id == "ea_channel_warning": entry_id = cast(str, data["entry_id"]) if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: api = async_create_api_client(hass, entry) diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 5611ba79eca7b4..e07a174659ccba 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -45,6 +45,7 @@ {"id": IRLEDMode.AUTO.value, "name": "Auto"}, {"id": IRLEDMode.ON.value, "name": "Always Enable"}, {"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"}, + {"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"}, {"id": IRLEDMode.OFF.value, "name": "Always Disable"}, ] diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index eccf5829332b6f..bdc46217ab54df 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -61,16 +61,16 @@ } }, "issues": { - "ea_warning": { - "title": "UniFi Protect v{version} is an Early Access version", + "ea_channel_warning": { + "title": "UniFi Protect Early Access enabled", "fix_flow": { "step": { "start": { - "title": "v{version} is an Early Access version", - "description": "You are using v{version} of UniFi Protect which is an Early Access version. [Early Access versions are not supported by Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) and it is recommended to go back to a stable release as soon as possible.\n\nBy submitting this form you have either [downgraded UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) or you agree to run an unsupported version of UniFi Protect." + "title": "UniFi Protect Early Access enabled", + "description": "You are either running an Early Access version of UniFi Protect (v{version}) or opt-ed into a release channel that is not the Official Release Channel. [Home Assistant does not support Early Access versions](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access), so you should immediately switch to the Official Release Channel. Accidentally upgrading to an Early Access version can break your UniFi Protect integration.\n\nBy submitting this form, you have switched back to the Official Release Channel or agree to run an unsupported version of UniFi Protect, which may break your Home Assistant integration at any time." }, "confirm": { - "title": "[%key:component::unifiprotect::issues::ea_warning::fix_flow::step::start::title%]", + "title": "[%key:component::unifiprotect::issues::ea_channel_warning::fix_flow::step::start::title%]", "description": "Are you sure you want to run unsupported versions of UniFi Protect? This may cause your Home Assistant integration to break." } } @@ -78,7 +78,7 @@ }, "ea_setup_failed": { "title": "Setup error using Early Access version", - "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}" + "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please restore a backup of a stable release of UniFi Protect to continue using the integration.\n\nError: {error}" }, "cloud_user": { "title": "Ubiquiti Cloud Users are not Supported", diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index 11aaf5307c8315..25e803e6a2d379 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["croniter"], "quality_scale": "internal", - "requirements": ["croniter==1.0.6"] + "requirements": ["croniter==2.0.2"] } diff --git a/homeassistant/const.py b/homeassistant/const.py index 847387e76ae558..f5efc37f352a2f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1b850316b91183..c92dad2ae3586e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==0.8.1 +aiodhcpwatcher==0.8.2 aiodiscover==1.6.1 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 diff --git a/pyproject.toml b/pyproject.toml index d8a1545fbb357f..496a29eb31c817 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.3.1" +version = "2024.3.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index add077d5b82f39..fe927e87424e72 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ ProgettiHWSW==0.1.3 # PyBluez==0.22 # homeassistant.components.cast -PyChromecast==14.0.0 +PyChromecast==14.0.1 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -206,7 +206,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.0 +aioautomower==2024.3.3 # homeassistant.components.azure_devops aioazuredevops==1.3.5 @@ -221,7 +221,7 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.1 +aiodhcpwatcher==0.8.2 # homeassistant.components.dhcp aiodiscover==1.6.1 @@ -350,7 +350,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.1 +aioraven==0.5.2 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==54 +axis==57 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 @@ -669,7 +669,7 @@ connect-box==0.2.8 construct==2.10.68 # homeassistant.components.utility_meter -croniter==1.0.6 +croniter==2.0.2 # homeassistant.components.crownstone crownstone-cloud==1.4.9 @@ -1115,7 +1115,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.1 +ical==7.0.3 # homeassistant.components.ping icmplib==3.0 @@ -1971,7 +1971,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.5 +pymodbus==3.6.6 # homeassistant.components.monoprice pymonoprice==0.4 @@ -2090,7 +2090,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.8 +pyrisco==0.5.10 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -2155,7 +2155,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.9 +pysnmp-lextudio==6.0.11 # homeassistant.components.snooz pysnooz==0.8.6 @@ -2182,7 +2182,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.16 +pytedee-async==0.2.17 # homeassistant.components.tfiac pytfiac==0.4 @@ -2340,7 +2340,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.23.3 +pyunifiprotect==5.0.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a50fefa22907dc..6307d864186dc0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ PlexAPI==4.15.10 ProgettiHWSW==0.1.3 # homeassistant.components.cast -PyChromecast==14.0.0 +PyChromecast==14.0.1 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -185,7 +185,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.0 +aioautomower==2024.3.3 # homeassistant.components.azure_devops aioazuredevops==1.3.5 @@ -200,7 +200,7 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.1 +aiodhcpwatcher==0.8.2 # homeassistant.components.dhcp aiodiscover==1.6.1 @@ -323,7 +323,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.1 +aioraven==0.5.2 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==54 +axis==57 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 @@ -553,7 +553,7 @@ colorthief==0.2.1 construct==2.10.68 # homeassistant.components.utility_meter -croniter==1.0.6 +croniter==2.0.2 # homeassistant.components.crownstone crownstone-cloud==1.4.9 @@ -905,7 +905,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.1 +ical==7.0.3 # homeassistant.components.ping icmplib==3.0 @@ -1525,7 +1525,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.5 +pymodbus==3.6.6 # homeassistant.components.monoprice pymonoprice==0.4 @@ -1617,7 +1617,7 @@ pyqwikswitch==0.93 pyrainbird==4.0.2 # homeassistant.components.risco -pyrisco==0.5.8 +pyrisco==0.5.10 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -1673,7 +1673,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.9 +pysnmp-lextudio==6.0.11 # homeassistant.components.snooz pysnooz==0.8.6 @@ -1697,7 +1697,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.16 +pytedee-async==0.2.17 # homeassistant.components.motionmount python-MotionMount==0.3.1 @@ -1801,7 +1801,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.23.3 +pyunifiprotect==5.0.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index f83f882b8a0205..3d83ecfcb7c35d 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -93,10 +93,11 @@ async def test_invalid_hostname(hass: HomeAssistant) -> None: assert result["errors"] == {CONF_HOST: "wrong_host"} -async def test_connection_error(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("exc", [ConnectionError, TimeoutError]) +async def test_connection_error(hass: HomeAssistant, exc: Exception) -> None: """Test connection to host error.""" with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=ConnectionError() + "brother.Brother._get_data", side_effect=exc ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -147,10 +148,11 @@ async def test_device_exists_abort(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_zeroconf_snmp_error(hass: HomeAssistant) -> None: - """Test we abort zeroconf flow on SNMP error.""" +@pytest.mark.parametrize("exc", [ConnectionError, TimeoutError, SnmpError("error")]) +async def test_zeroconf_exception(hass: HomeAssistant, exc: Exception) -> None: + """Test we abort zeroconf flow on exception.""" with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=SnmpError("error") + "brother.Brother._get_data", side_effect=exc ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 1775caac7f8ccb..feae870478ed77 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -5,6 +5,7 @@ from aioautomower.model import MowerModes from aioautomower.utils import mower_list_to_dictionary_dataclass from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN @@ -59,17 +60,36 @@ async def test_cutting_blade_usage_time_sensor( assert state is not None assert state.state == "0.034" - entry = hass.config_entries.async_entries(DOMAIN)[0] - await hass.config_entries.async_remove(entry.entry_id) - await hass.async_block_till_done() + +@pytest.mark.parametrize( + ("sensor_to_test"), + [ + ("cutting_blade_usage_time"), + ("number_of_charging_cycles"), + ("number_of_collisions"), + ("total_charging_time"), + ("total_cutting_time"), + ("total_running_time"), + ("total_searching_time"), + ("total_drive_distance"), + ], +) +async def test_statistics_not_available( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + sensor_to_test: str, +) -> None: + """Test if this sensor is only added, if data is available.""" + values = mower_list_to_dictionary_dataclass( load_json_value_fixture("mower.json", DOMAIN) ) - delattr(values[TEST_MOWER_ID].statistics, "cutting_blade_usage_time") + delattr(values[TEST_MOWER_ID].statistics, sensor_to_test) mock_automower_client.get_status.return_value = values await setup_integration(hass, mock_config_entry) - state = hass.states.get("sensor.test_mower_1_cutting_blade_usage_time") + state = hass.states.get(f"sensor.test_mower_1_{sensor_to_test}") assert state is None diff --git a/tests/components/local_todo/snapshots/test_todo.ambr b/tests/components/local_todo/snapshots/test_todo.ambr index db4403f301cc57..15a44ff8c2700a 100644 --- a/tests/components/local_todo/snapshots/test_todo.ambr +++ b/tests/components/local_todo/snapshots/test_todo.ambr @@ -22,6 +22,16 @@ list([ ]) # --- +# name: test_parse_existing_ics[invalid_dtstart_tzname] + list([ + dict({ + 'due': '2023-10-24T11:30:00', + 'status': 'needs_action', + 'summary': 'Task', + 'uid': '077cb7f2-6c89-11ee-b2a9-0242ac110002', + }), + ]) +# --- # name: test_parse_existing_ics[migrate_legacy_due] list([ dict({ diff --git a/tests/components/local_todo/test_todo.py b/tests/components/local_todo/test_todo.py index 231f56b0afb744..760b0260dbbfea 100644 --- a/tests/components/local_todo/test_todo.py +++ b/tests/components/local_todo/test_todo.py @@ -671,6 +671,28 @@ async def test_move_item_previous_unknown( ), "1", ), + ( + textwrap.dedent( + """\ + BEGIN:VCALENDAR + PRODID:-//homeassistant.io//local_todo 2.0//EN + VERSION:2.0 + BEGIN:VTODO + DTSTAMP:20231024T014011 + UID:077cb7f2-6c89-11ee-b2a9-0242ac110002 + CREATED:20231017T010348 + LAST-MODIFIED:20231024T014011 + SEQUENCE:1 + STATUS:NEEDS-ACTION + SUMMARY:Task + DUE:20231024T113000 + DTSTART;TZID=CST:20231024T113000 + END:VTODO + END:VCALENDAR + """ + ), + "1", + ), ], ids=( "empty", @@ -679,6 +701,7 @@ async def test_move_item_previous_unknown( "needs_action", "migrate_legacy_due", "due", + "invalid_dtstart_tzname", ), ) async def test_parse_existing_ics( diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 3da37a72459a71..cb9fca54f3d117 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -155,7 +155,9 @@ {ATTR_INJECT_READS: b" 1"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 0"}, + { + ATTR_INJECT_READS: ProtocolError, + }, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 0"}, @@ -165,7 +167,9 @@ {ATTR_INJECT_READS: b" 1"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, + { + ATTR_INJECT_READS: ProtocolError, + }, {ATTR_INJECT_READS: b" 1"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 1"}, diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 2aa415f0345112..0523c969ade5d6 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -851,13 +851,13 @@ 'attributes': ReadOnlyDict({ 'device_file': '/29.111111111111/sensed.3', 'friendly_name': '29.111111111111 Sensed 3', - 'raw_value': 0.0, + 'raw_value': None, }), 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_3', 'last_changed': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 8fbb977948b61c..4f6498419a94f9 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -1271,13 +1271,13 @@ 'attributes': ReadOnlyDict({ 'device_file': '/29.111111111111/PIO.3', 'friendly_name': '29.111111111111 Programmed input-output 3', - 'raw_value': 0.0, + 'raw_value': None, }), 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_3', 'last_changed': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index a8a764cd50279f..e08e6b29852a17 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -171,16 +171,6 @@ def connect_with_error(exception): yield -@pytest.fixture -def connect_with_single_error(exception): - """Fixture to simulate error on connect.""" - with patch( - "homeassistant.components.risco.RiscoLocal.connect", - side_effect=[exception, None], - ): - yield - - @pytest.fixture async def setup_risco_local(hass, local_config_entry): """Set up a local Risco integration for testing.""" diff --git a/tests/components/risco/test_init.py b/tests/components/risco/test_init.py deleted file mode 100644 index a1a9e3bd6a7ac8..00000000000000 --- a/tests/components/risco/test_init.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Tests for the Risco initialization.""" -import pytest - -from homeassistant.components.risco import CannotConnectError -from homeassistant.components.risco.const import CONF_COMMUNICATION_DELAY -from homeassistant.core import HomeAssistant - - -@pytest.mark.parametrize("exception", [CannotConnectError]) -async def test_single_error_on_connect( - hass: HomeAssistant, connect_with_single_error, local_config_entry -) -> None: - """Test single error on connect to validate communication delay update from 0 (default) to 1.""" - expected_data = { - **local_config_entry.data, - **{"type": "local", CONF_COMMUNICATION_DELAY: 1}, - } - - await hass.config_entries.async_setup(local_config_entry.entry_id) - await hass.async_block_till_done() - assert local_config_entry.data == expected_data diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 67df09a5adb912..940ab2123f0dd1 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -67,6 +67,18 @@ async def test_block_reload_on_cfg_change( mock_block_device.mock_update() await hass.async_block_till_done() + # Make sure cfgChanged with None is ignored + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", None) + mock_block_device.mock_update() + await hass.async_block_till_done() + + # Wait for debouncer + freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1") is not None + # Generate config change from switch to light monkeypatch.setitem( mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light" diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 314218fc8490c2..8026618e7cd608 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -29,6 +29,7 @@ from tests.common import ( MockConfigEntry, assert_setup_component, + async_capture_events, async_fire_time_changed, mock_restore_cache_with_extra_data, ) @@ -1848,6 +1849,7 @@ async def test_trigger_entity_restore_state( "my_variable": "{{ trigger.event.data.beer + 1 }}" }, }, + {"event": "test_event2", "event_data": {"hello": "world"}}, ], "sensor": [ { @@ -1864,6 +1866,10 @@ async def test_trigger_action( hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry ) -> None: """Test trigger entity with an action works.""" + event = "test_event2" + context = Context() + events = async_capture_events(hass, event) + state = hass.states.get("sensor.hello_name") assert state is not None assert state.state == STATE_UNKNOWN @@ -1875,3 +1881,6 @@ async def test_trigger_action( state = hass.states.get("sensor.hello_name") assert state.state == "3" assert state.context is context + + assert len(events) == 1 + assert events[0].context.parent_id == context.id diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr index 1726f1c3d45e89..20d01e427ea088 100644 --- a/tests/components/traccar_server/snapshots/test_diagnostics.ambr +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -34,7 +34,7 @@ 'uniqueId': 'abc123', }), 'geofence': dict({ - 'area': 'string', + 'area': '**REDACTED**', 'attributes': dict({ }), 'calendarId': 0, @@ -134,7 +134,7 @@ 'uniqueId': 'abc123', }), 'geofence': dict({ - 'area': 'string', + 'area': '**REDACTED**', 'attributes': dict({ }), 'calendarId': 0, diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index a9ff98fc681142..04eee1b8319a99 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -318,7 +318,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "disable_rtsp": True, "override_connection_host": True, "max_media": 1000, - "allow_ea": False, + "allow_ea_channel": False, } await hass.config_entries.async_unload(mock_config.entry_id) diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index 1270160430681b..0c939a9791de42 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -45,12 +45,14 @@ async def test_ea_warning_ignore( assert len(msg["result"]["issues"]) > 0 issue = None for i in msg["result"]["issues"]: - if i["issue_id"] == "ea_warning": + if i["issue_id"] == "ea_channel_warning": issue = i assert issue is not None url = RepairsFlowIndexView.url - resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + resp = await client.post( + url, json={"handler": DOMAIN, "issue_id": "ea_channel_warning"} + ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -103,12 +105,14 @@ async def test_ea_warning_fix( assert len(msg["result"]["issues"]) > 0 issue = None for i in msg["result"]["issues"]: - if i["issue_id"] == "ea_warning": + if i["issue_id"] == "ea_channel_warning": issue = i assert issue is not None url = RepairsFlowIndexView.url - resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + resp = await client.post( + url, json={"handler": DOMAIN, "issue_id": "ea_channel_warning"} + ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -121,8 +125,9 @@ async def test_ea_warning_fix( new_nvr = copy(ufp.api.bootstrap.nvr) new_nvr.version = Version("2.2.6") + new_nvr.release_channel = "release" mock_msg = Mock() - mock_msg.changed_data = {"version": "2.2.6"} + mock_msg.changed_data = {"version": "2.2.6", "releaseChannel": "release"} mock_msg.new_obj = new_nvr ufp.api.bootstrap.nvr = new_nvr