diff --git a/CODEOWNERS b/CODEOWNERS index e426d5f98b5..684623113db 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -783,6 +783,7 @@ build.json @home-assistant/supervisor /homeassistant/components/netdata/ @fabaff /homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG /tests/components/netgear/ @hacf-fr @Quentame @starkillerOG +/homeassistant/components/netgear_lte/ @tkdrob /homeassistant/components/network/ @home-assistant/core /tests/components/network/ @home-assistant/core /homeassistant/components/nexia/ @bdraco diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index c344b1ff49c..1a167f49f78 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -3,12 +3,12 @@ from __future__ import annotations from typing import Any, Final -from aioairzone.common import OperationMode +from aioairzone.common import OperationAction, OperationMode from aioairzone.const import ( API_MODE, API_ON, API_SET_POINT, - AZD_DEMAND, + AZD_ACTION, AZD_HUMIDITY, AZD_MASTER, AZD_MODE, @@ -39,12 +39,13 @@ from .const import API_TEMPERATURE_STEP, DOMAIN, TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneZoneEntity -HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationMode, HVACAction]] = { - OperationMode.STOP: HVACAction.OFF, - OperationMode.COOLING: HVACAction.COOLING, - OperationMode.HEATING: HVACAction.HEATING, - OperationMode.FAN: HVACAction.FAN, - OperationMode.DRY: HVACAction.DRYING, +HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationAction, HVACAction]] = { + OperationAction.COOLING: HVACAction.COOLING, + OperationAction.DRYING: HVACAction.DRYING, + OperationAction.FAN: HVACAction.FAN, + OperationAction.HEATING: HVACAction.HEATING, + OperationAction.IDLE: HVACAction.IDLE, + OperationAction.OFF: HVACAction.OFF, } HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, HVACMode]] = { OperationMode.STOP: HVACMode.OFF, @@ -156,14 +157,13 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): """Update climate attributes.""" self._attr_current_temperature = self.get_airzone_value(AZD_TEMP) self._attr_current_humidity = self.get_airzone_value(AZD_HUMIDITY) + self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[ + self.get_airzone_value(AZD_ACTION) + ] if self.get_airzone_value(AZD_ON): - mode = self.get_airzone_value(AZD_MODE) - self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[mode] - if self.get_airzone_value(AZD_DEMAND): - self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[mode] - else: - self._attr_hvac_action = HVACAction.IDLE + self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[ + self.get_airzone_value(AZD_MODE) + ] else: - self._attr_hvac_action = HVACAction.OFF self._attr_hvac_mode = HVACMode.OFF self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 08d7fb1aced..991584dd8f8 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.5.2"] + "requirements": ["aioairzone==0.5.5"] } diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 159bfebc624..e086d525cf1 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -17,11 +17,12 @@ _LOGGER = logging.getLogger(__name__) class AbstractConfig(ABC): """Hold the configuration for Alexa.""" - _unsub_proactive_report: asyncio.Task[CALLBACK_TYPE] | None = None + _unsub_proactive_report: CALLBACK_TYPE | None = None def __init__(self, hass: HomeAssistant) -> None: """Initialize abstract config.""" self.hass = hass + self._enable_proactive_mode_lock = asyncio.Lock() self._store = None async def async_initialize(self): @@ -67,20 +68,17 @@ class AbstractConfig(ABC): async def async_enable_proactive_mode(self): """Enable proactive mode.""" _LOGGER.debug("Enable proactive mode") - if self._unsub_proactive_report is None: - self._unsub_proactive_report = self.hass.async_create_task( - async_enable_proactive_mode(self.hass, self) + async with self._enable_proactive_mode_lock: + if self._unsub_proactive_report is not None: + return + self._unsub_proactive_report = await async_enable_proactive_mode( + self.hass, self ) - try: - await self._unsub_proactive_report - except Exception: - self._unsub_proactive_report = None - raise async def async_disable_proactive_mode(self): """Disable proactive mode.""" _LOGGER.debug("Disable proactive mode") - if unsub_func := await self._unsub_proactive_report: + if unsub_func := self._unsub_proactive_report: unsub_func() self._unsub_proactive_report = None diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index fb4cc002598..4b957674655 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak==0.20.2", "bleak-retry-connector==3.0.2", "bluetooth-adapters==0.15.3", - "bluetooth-auto-recovery==1.1.2", + "bluetooth-auto-recovery==1.2.0", "bluetooth-data-tools==0.4.0", "dbus-fast==1.85.0" ] diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 3c7d2ba27c3..afabcbd3df4 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer_connected==0.13.2"] + "requirements": ["bimmer_connected==0.13.3"] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ff78996f3aa..0e9715038f0 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==13.7.3", + "aioesphomeapi==13.7.4", "bluetooth-data-tools==0.4.0", "esphome-dashboard-api==1.2.3" ], diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 821b53f7e12..60b422eff2f 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -283,7 +283,7 @@ class FritzBoxTools( entity_data["entity_states"][ key ] = await self.hass.async_add_executor_job( - update_fn, self.fritz_status, self.data.get(key) + update_fn, self.fritz_status, self.data["entity_states"].get(key) ) if self.has_call_deflections: entity_data[ diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index d199b8808d3..d55a1136646 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -174,23 +174,23 @@ class IntegrationSensor(RestoreEntity, SensorEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - if state := await self.async_get_last_state(): - try: - self._state = Decimal(state.state) - except (DecimalException, ValueError) as err: - _LOGGER.warning( - "%s could not restore last state %s: %s", - self.entity_id, - state.state, - err, - ) - else: - self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) - if self._unit_of_measurement is None: - self._unit_of_measurement = state.attributes.get( - ATTR_UNIT_OF_MEASUREMENT + if (state := await self.async_get_last_state()) is not None: + if state.state == STATE_UNAVAILABLE: + self._attr_available = False + elif state.state != STATE_UNKNOWN: + try: + self._state = Decimal(state.state) + except (DecimalException, ValueError) as err: + _LOGGER.warning( + "%s could not restore last state %s: %s", + self.entity_id, + state.state, + err, ) + self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) + self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + @callback def calc_integration(event: Event) -> None: """Handle the sensor state changes.""" diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index f8964e78f63..e668a7ad79a 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -11,6 +11,7 @@ from typing import Any, cast from aiolifx.aiolifx import ( Light, + Message, MultiZoneDirection, MultiZoneEffectType, TileEffectType, @@ -56,6 +57,8 @@ from .util import ( LIGHT_UPDATE_INTERVAL = 10 REQUEST_REFRESH_DELAY = 0.35 LIFX_IDENTIFY_DELAY = 3.0 +ZONES_PER_COLOR_UPDATE_REQUEST = 8 + RSSI_DBM_FW = AwesomeVersion("2.77") @@ -208,18 +211,50 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]): def get_number_of_zones(self) -> int: """Return the number of zones. - If the number of zones is not yet populated, return 0 + If the number of zones is not yet populated, return 1 since + the device will have a least one zone. """ - return len(self.device.color_zones) if self.device.color_zones else 0 + return len(self.device.color_zones) if self.device.color_zones else 1 @callback def _async_build_color_zones_update_requests(self) -> list[Callable]: """Build a color zones update request.""" device = self.device - return [ - partial(device.get_color_zones, start_index=zone) - for zone in range(0, self.get_number_of_zones(), 8) - ] + calls: list[Callable] = [] + for zone in range( + 0, self.get_number_of_zones(), ZONES_PER_COLOR_UPDATE_REQUEST + ): + + def _wrap_get_color_zones( + callb: Callable[[Message, dict[str, Any] | None], None], + get_color_zones_args: dict[str, Any], + ) -> None: + """Capture the callback and make sure resp_set_multizonemultizone is called before.""" + + def _wrapped_callback( + bulb: Light, + response: Message, + **kwargs: Any, + ) -> None: + # We need to call resp_set_multizonemultizone to populate + # the color_zones attribute before calling the callback + device.resp_set_multizonemultizone(response) + # Now call the original callback + callb(bulb, response, **kwargs) + + device.get_color_zones(**get_color_zones_args, callb=_wrapped_callback) + + calls.append( + partial( + _wrap_get_color_zones, + get_color_zones_args={ + "start_index": zone, + "end_index": zone + ZONES_PER_COLOR_UPDATE_REQUEST - 1, + }, + ) + ) + + return calls async def _async_update_data(self) -> None: """Fetch all device data from the api.""" diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index c9adac23186..bb92496b74f 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -51,7 +51,7 @@ PLATFORMS = [ ] -async def with_timeout(task, timeout_seconds=10): +async def with_timeout(task, timeout_seconds=30): """Run an async task with a timeout.""" async with async_timeout.timeout(timeout_seconds): return await task diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 427aa9633c8..c9a5245da41 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -1,9 +1,9 @@ { "domain": "netgear_lte", "name": "NETGEAR LTE", - "codeowners": [], + "codeowners": ["@tkdrob"], "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "iot_class": "local_polling", "loggers": ["eternalegypt"], - "requirements": ["eternalegypt==0.0.15"] + "requirements": ["eternalegypt==0.0.16"] } diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 5e55496fc54..ad228f08a4b 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from dataclasses import dataclass, field, fields +from dataclasses import dataclass, field from datetime import timedelta import logging import traceback @@ -10,9 +10,16 @@ from typing import Any from uuid import UUID from aionotion import async_get_client -from aionotion.bridge.models import Bridge +from aionotion.bridge.models import Bridge, BridgeAllResponse from aionotion.errors import InvalidCredentialsError, NotionError -from aionotion.sensor.models import Listener, ListenerKind, Sensor +from aionotion.sensor.models import ( + Listener, + ListenerAllResponse, + ListenerKind, + Sensor, + SensorAllResponse, +) +from aionotion.user.models import UserPreferences, UserPreferencesResponse from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -51,6 +58,11 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] ATTR_SYSTEM_MODE = "system_mode" ATTR_SYSTEM_NAME = "system_name" +DATA_BRIDGES = "bridges" +DATA_LISTENERS = "listeners" +DATA_SENSORS = "sensors" +DATA_USER_PREFERENCES = "user_preferences" + DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -84,6 +96,9 @@ def is_uuid(value: str) -> bool: class NotionData: """Define a manager class for Notion data.""" + hass: HomeAssistant + entry: ConfigEntry + # Define a dict of bridges, indexed by bridge ID (an integer): bridges: dict[int, Bridge] = field(default_factory=dict) @@ -93,12 +108,40 @@ class NotionData: # Define a dict of sensors, indexed by sensor UUID (a string): sensors: dict[str, Sensor] = field(default_factory=dict) + # Define a user preferences response object: + user_preferences: UserPreferences | None = field(default=None) + + def update_data_from_response( + self, + response: BridgeAllResponse + | ListenerAllResponse + | SensorAllResponse + | UserPreferencesResponse, + ) -> None: + """Update data from an aionotion response.""" + if isinstance(response, BridgeAllResponse): + for bridge in response.bridges: + # If a new bridge is discovered, register it: + if bridge.id not in self.bridges: + _async_register_new_bridge(self.hass, self.entry, bridge) + self.bridges[bridge.id] = bridge + elif isinstance(response, ListenerAllResponse): + self.listeners = {listener.id: listener for listener in response.listeners} + elif isinstance(response, SensorAllResponse): + self.sensors = {sensor.uuid: sensor for sensor in response.sensors} + elif isinstance(response, UserPreferencesResponse): + self.user_preferences = response.user_preferences + def asdict(self) -> dict[str, Any]: """Represent this dataclass (and its Pydantic contents) as a dict.""" - return { - field.name: [obj.dict() for obj in getattr(self, field.name).values()] - for field in fields(self) + data: dict[str, Any] = { + DATA_BRIDGES: [bridge.dict() for bridge in self.bridges.values()], + DATA_LISTENERS: [listener.dict() for listener in self.listeners.values()], + DATA_SENSORS: [sensor.dict() for sensor in self.sensors.values()], } + if self.user_preferences: + data[DATA_USER_PREFERENCES] = self.user_preferences.dict() + return data async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -121,11 +164,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update() -> NotionData: """Get the latest data from the Notion API.""" - data = NotionData() + data = NotionData(hass=hass, entry=entry) tasks = { - "bridges": client.bridge.async_all(), - "listeners": client.sensor.async_listeners(), - "sensors": client.sensor.async_all(), + DATA_BRIDGES: client.bridge.async_all(), + DATA_LISTENERS: client.sensor.async_listeners(), + DATA_SENSORS: client.sensor.async_all(), + DATA_USER_PREFERENCES: client.user.async_preferences(), } results = await asyncio.gather(*tasks.values(), return_exceptions=True) @@ -145,16 +189,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"There was an unknown error while updating {attr}: {result}" ) from result - for item in result: - if attr == "bridges": - # If a new bridge is discovered, register it: - if item.id not in data.bridges: - _async_register_new_bridge(hass, item, entry) - data.bridges[item.id] = item - elif attr == "listeners": - data.listeners[item.id] = item - else: - data.sensors[item.uuid] = item + data.update_data_from_response(result) return data @@ -216,7 +251,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_register_new_bridge( - hass: HomeAssistant, bridge: Bridge, entry: ConfigEntry + hass: HomeAssistant, entry: ConfigEntry, bridge: Bridge ) -> None: """Register a new bridge.""" if name := bridge.name: @@ -279,6 +314,11 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): and self._listener_id in self.coordinator.data.listeners ) + @property + def listener(self) -> Listener: + """Return the listener related to this entity.""" + return self.coordinator.data.listeners[self._listener_id] + @callback def _async_update_bridge_id(self) -> None: """Update the entity's bridge ID if it has changed. @@ -310,21 +350,9 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): this_device.id, via_device_id=bridge_device.id ) - @callback - def _async_update_from_latest_data(self) -> None: - """Update the entity from the latest data.""" - raise NotImplementedError - @callback def _handle_coordinator_update(self) -> None: """Respond to a DataUpdateCoordinator update.""" if self._listener_id in self.coordinator.data.listeners: self._async_update_bridge_id() - self._async_update_from_latest_data() - - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - await super().async_added_to_hass() - self._async_update_from_latest_data() + super()._handle_coordinator_update() diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index bd2de303d2d..f70af18c3e1 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NotionEntity @@ -37,7 +37,7 @@ from .model import NotionEntityDescriptionMixin class NotionBinarySensorDescriptionMixin: """Define an entity description mixin for binary and regular sensors.""" - on_state: Literal["alarm", "critical", "leak", "not_missing", "open"] + on_state: Literal["alarm", "leak", "low", "not_missing", "open"] @dataclass @@ -56,7 +56,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, listener_kind=ListenerKind.BATTERY, - on_state="critical", + on_state="low", ), NotionBinarySensorDescription( key=SENSOR_DOOR, @@ -146,17 +146,10 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): entity_description: NotionBinarySensorDescription - @callback - def _async_update_from_latest_data(self) -> None: - """Fetch new state data for the sensor.""" - listener = self.coordinator.data.listeners[self._listener_id] - - if listener.status.trigger_value: - state = listener.status.trigger_value - elif listener.insights.primary.value: - state = listener.insights.primary.value - else: - LOGGER.warning("Unknown listener structure: %s", listener) - state = None - - self._attr_is_on = self.entity_description.on_state == state + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + if not self.listener.insights.primary.value: + LOGGER.warning("Unknown listener structure: %s", self.listener.dict()) + return False + return self.listener.insights.primary.value == self.entity_description.on_state diff --git a/homeassistant/components/notion/diagnostics.py b/homeassistant/components/notion/diagnostics.py index 06100580b39..86b84760016 100644 --- a/homeassistant/components/notion/diagnostics.py +++ b/homeassistant/components/notion/diagnostics.py @@ -16,6 +16,7 @@ CONF_DEVICE_KEY = "device_key" CONF_HARDWARE_ID = "hardware_id" CONF_LAST_BRIDGE_HARDWARE_ID = "last_bridge_hardware_id" CONF_TITLE = "title" +CONF_USER_ID = "user_id" TO_REDACT = { CONF_DEVICE_KEY, @@ -27,6 +28,7 @@ TO_REDACT = { CONF_TITLE, CONF_UNIQUE_ID, CONF_USERNAME, + CONF_USER_ID, } diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 1c3ffc8607a..168899c38e0 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aionotion"], - "requirements": ["aionotion==2023.05.0"] + "requirements": ["aionotion==2023.05.4"] } diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index f4e6e7cc322..e6ff3eaab69 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -11,11 +11,11 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NotionEntity -from .const import DOMAIN, LOGGER, SENSOR_TEMPERATURE +from .const import DOMAIN, SENSOR_TEMPERATURE from .model import NotionEntityDescriptionMixin @@ -63,15 +63,24 @@ async def async_setup_entry( class NotionSensor(NotionEntity, SensorEntity): """Define a Notion sensor.""" - @callback - def _async_update_from_latest_data(self) -> None: - """Fetch new state data for the sensor.""" - listener = self.coordinator.data.listeners[self._listener_id] + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor.""" + if self.listener.listener_kind == ListenerKind.TEMPERATURE: + if not self.coordinator.data.user_preferences: + return None + if self.coordinator.data.user_preferences.celsius_enabled: + return UnitOfTemperature.CELSIUS + return UnitOfTemperature.FAHRENHEIT + return None - if listener.listener_kind == ListenerKind.TEMPERATURE: - self._attr_native_value = round(listener.status.temperature, 1) # type: ignore[attr-defined] - else: - LOGGER.error( - "Unknown listener type for sensor %s", - self.coordinator.data.sensors[self._sensor_id], - ) + @property + def native_value(self) -> str | None: + """Return the value reported by the sensor. + + The Notion API only returns a localized string for temperature (e.g. "70°"); we + simply remove the degree symbol: + """ + if not self.listener.status_localized: + return None + return self.listener.status_localized.state[:-1] diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 2c96b79cbeb..a7c23064f64 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,5 +1,6 @@ """The ONVIF integration.""" import asyncio +from http import HTTPStatus import logging from httpx import RequestError @@ -56,7 +57,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except ONVIFError as err: await device.device.close() raise ConfigEntryNotReady( - f"Could not setup camera {device.device.host}:{device.device.port}: {err}" + f"Could not setup camera {device.device.host}:{device.device.port}: {stringify_onvif_error(err)}" + ) from err + except TransportError as err: + await device.device.close() + stringified_onvif_error = stringify_onvif_error(err) + if err.status_code in ( + HTTPStatus.UNAUTHORIZED.value, + HTTPStatus.FORBIDDEN.value, + ): + raise ConfigEntryAuthFailed( + f"Auth Failed: {stringified_onvif_error}" + ) from err + raise ConfigEntryNotReady( + f"Could not setup camera {device.device.host}:{device.device.port}: {stringified_onvif_error}" ) from err except asyncio.CancelledError as err: # After https://github.com/agronholm/anyio/issues/374 is resolved diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 27f279266dd..ca447c71b84 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -142,10 +142,14 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): hass.async_create_task(hass.config_entries.async_reload(entry_id)) return self.async_abort(reason="reauth_successful") + username = (user_input or {}).get(CONF_USERNAME) or entry.data[CONF_USERNAME] return self.async_show_form( step_id="reauth_confirm", data_schema=vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME, default=username): str, + vol.Required(CONF_PASSWORD): str, + } ), errors=errors, description_placeholders=description_placeholders, diff --git a/homeassistant/components/onvif/diagnostics.py b/homeassistant/components/onvif/diagnostics.py index d7f2c515308..a802aed5e80 100644 --- a/homeassistant/components/onvif/diagnostics.py +++ b/homeassistant/components/onvif/diagnostics.py @@ -27,6 +27,10 @@ async def async_get_config_entry_diagnostics( "info": asdict(device.info), "capabilities": asdict(device.capabilities), "profiles": [asdict(profile) for profile in device.profiles], + "services": { + str(key): service.url for key, service in device.device.services.items() + }, + "xaddrs": device.device.xaddrs, } data["events"] = { "webhook_manager_state": device.events.webhook_manager.state, diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json index 55413e4bf6c..3e9db0b3c7e 100644 --- a/homeassistant/components/onvif/strings.json +++ b/homeassistant/components/onvif/strings.json @@ -47,6 +47,7 @@ }, "reauth_confirm": { "title": "Reauthenticate the ONVIF device", + "description": "Some devices will reject authentication if the time is out of sync by more than 5 seconds. If authentication is unsuccessful, verify the time on the device is correct and try again.", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index caa4f6c3868..dfd4a6c28e1 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -13,7 +13,7 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.7.7"], + "requirements": ["pyoverkiz==1.7.8"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json index 5ec3a6ae190..0b5640fe3a4 100644 --- a/homeassistant/components/rdw/manifest.json +++ b/homeassistant/components/rdw/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["vehicle==1.0.0"] + "requirements": ["vehicle==1.0.1"] } diff --git a/homeassistant/components/reolink/button.py b/homeassistant/components/reolink/button.py index 65bb8036c0b..3aa5faa527b 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -46,28 +46,28 @@ BUTTON_ENTITIES = ( key="ptz_left", name="PTZ left", icon="mdi:pan", - supported=lambda api, ch: api.supported(ch, "pan_tilt"), + supported=lambda api, ch: api.supported(ch, "pan"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.left.value), ), ReolinkButtonEntityDescription( key="ptz_right", name="PTZ right", icon="mdi:pan", - supported=lambda api, ch: api.supported(ch, "pan_tilt"), + supported=lambda api, ch: api.supported(ch, "pan"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.right.value), ), ReolinkButtonEntityDescription( key="ptz_up", name="PTZ up", icon="mdi:pan", - supported=lambda api, ch: api.supported(ch, "pan_tilt"), + supported=lambda api, ch: api.supported(ch, "tilt"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.up.value), ), ReolinkButtonEntityDescription( key="ptz_down", name="PTZ down", icon="mdi:pan", - supported=lambda api, ch: api.supported(ch, "pan_tilt"), + supported=lambda api, ch: api.supported(ch, "tilt"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.down.value), ), ReolinkButtonEntityDescription( diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index cad89ac48c1..6a4ae98a154 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.5.13"] + "requirements": ["reolink-aio==0.5.15"] } diff --git a/homeassistant/components/reolink/siren.py b/homeassistant/components/reolink/siren.py index 405c3e2716d..9dba3b840ea 100644 --- a/homeassistant/components/reolink/siren.py +++ b/homeassistant/components/reolink/siren.py @@ -35,7 +35,7 @@ SIREN_ENTITIES = ( key="siren", name="Siren", icon="mdi:alarm-light", - supported=lambda api, ch: api.supported(ch, "siren"), + supported=lambda api, ch: api.supported(ch, "siren_play"), ), ) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 497d30b41cf..1ea5e4734bb 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -8,6 +8,7 @@ import logging from roborock.api import RoborockApiClient from roborock.cloud_api import RoborockMqttClient from roborock.containers import HomeDataDevice, RoborockDeviceInfo, UserData +from roborock.exceptions import RoborockException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME @@ -44,7 +45,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for device, result in zip(devices, network_results) if result is not None } - await mqtt_client.async_disconnect() + try: + await mqtt_client.async_disconnect() + except RoborockException as err: + _LOGGER.warning("Failed disconnecting from the mqtt server %s", err) if not network_info: raise ConfigEntryNotReady( "Could not get network information about your devices" diff --git a/homeassistant/components/sia/__init__.py b/homeassistant/components/sia/__init__.py index befa2c5df92..a59d1f1cdad 100644 --- a/homeassistant/components/sia/__init__.py +++ b/homeassistant/components/sia/__init__.py @@ -17,7 +17,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = hub try: if hub.sia_client: - await hub.sia_client.start(reuse_port=True) + await hub.sia_client.async_start(reuse_port=True) except OSError as exc: raise ConfigEntryNotReady( f"SIA Server at port {entry.data[CONF_PORT]} could not start." diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index fb8d20e1830..64ca3832ce0 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -71,7 +71,7 @@ class SIAHub: async def async_shutdown(self, _: Event | None = None) -> None: """Shutdown the SIA server.""" if self.sia_client: - await self.sia_client.stop() + await self.sia_client.async_stop() async def async_create_and_fire_event(self, event: SIAEvent) -> None: """Create a event on HA dispatcher and then on HA's bus, with the data from the SIAEvent. diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 8b6deaa3c7a..3d757e2328d 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/sleepiq", "iot_class": "cloud_polling", "loggers": ["asyncsleepiq"], - "requirements": ["asyncsleepiq==1.3.4"] + "requirements": ["asyncsleepiq==1.3.5"] } diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 4a05053940c..087c636f1ed 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/sonos", "iot_class": "local_push", "loggers": ["soco"], - "requirements": ["soco==0.29.1", "sonos-websocket==0.1.0"], + "requirements": ["soco==0.29.1", "sonos-websocket==0.1.1"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index c17a26794df..ecda3addcb5 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -147,8 +147,10 @@ async def async_remove_config_entry_device( api = data.api serial = api.information.serial storage = api.storage - # get_all_cameras does not do I/O - all_cameras: list[SynoCamera] = api.surveillance_station.get_all_cameras() + all_cameras: list[SynoCamera] = [] + if api.surveillance_station is not None: + # get_all_cameras does not do I/O + all_cameras = api.surveillance_station.get_all_cameras() device_ids = chain( (camera.id for camera in all_cameras), storage.volumes_ids, diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index 3702751ef44..00cebe1e0d9 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/upb", "iot_class": "local_push", "loggers": ["upb_lib"], - "requirements": ["upb_lib==0.5.3"] + "requirements": ["upb_lib==0.5.4"] } diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 99553426ea8..89a35ecde1d 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/volvooncall", "iot_class": "cloud_polling", "loggers": ["geopy", "hbmqtt", "volvooncall"], - "requirements": ["volvooncall==0.10.2"] + "requirements": ["volvooncall==0.10.3"] } diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 1eac959c169..9152739852e 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "loggers": ["aiowebostv"], "quality_scale": "platinum", - "requirements": ["aiowebostv==0.3.2"], + "requirements": ["aiowebostv==0.3.3"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index f34af9ff913..61f59fe06d6 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "incorrect_province": "Incorrect subdivision from yaml import" + "incorrect_province": "Incorrect subdivision from yaml import", + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }, "step": { "user": { @@ -31,8 +32,7 @@ }, "error": { "add_holiday_error": "Incorrect format on date (YYYY-MM-DD)", - "remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found" } }, "options": { @@ -59,7 +59,7 @@ "error": { "add_holiday_error": "Incorrect format on date (YYYY-MM-DD)", "remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "Service with this configuration already exist" } }, "issues": { diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9407dc84147..46fe2ce472a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,7 +20,7 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.2", + "bellows==0.35.5", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.99", diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index de9d4842ff7..b9209c6904f 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -84,7 +84,7 @@ bulk_set_partial_config_parameters: value: name: Value description: The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter. - example: + example: | "0x1": 1 "0x10": 1 "0x20": 1 @@ -287,7 +287,7 @@ invoke_cc_api: parameters: name: Parameters description: A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters. - example: [1, 1] + example: "[1, 1]" required: true selector: object: diff --git a/homeassistant/const.py b/homeassistant/const.py index 7c9681ff2b4..05e9808473a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 78a8051df1c..78806cb5ae1 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -37,6 +37,11 @@ SERVER_SOFTWARE = "{0}/{1} aiohttp/{2} Python/{3[0]}.{3[1]}".format( APPLICATION_NAME, __version__, aiohttp.__version__, sys.version_info ) +ENABLE_CLEANUP_CLOSED = sys.version_info < (3, 11, 1) +# Enabling cleanup closed on python 3.11.1+ leaks memory relatively quickly +# see https://github.com/aio-libs/aiohttp/issues/7252 +# aiohttp interacts poorly with https://github.com/python/cpython/pull/98540 + WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session" # @@ -276,7 +281,7 @@ def _async_get_connector( ssl_context = ssl_util.get_default_no_verify_context() connector = aiohttp.TCPConnector( - enable_cleanup_closed=True, + enable_cleanup_closed=ENABLE_CLEANUP_CLOSED, ssl=ssl_context, limit=MAXIMUM_CONNECTIONS, limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 9dbd5d4ad67..00171350594 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -763,13 +763,6 @@ class Entity(ABC): hass = self.hass assert hass is not None - if hasattr(self, "async_update"): - coro: asyncio.Future[None] = self.async_update() - elif hasattr(self, "update"): - coro = hass.async_add_executor_job(self.update) - else: - return - self._update_staged = True # Process update sequential @@ -780,8 +773,14 @@ class Entity(ABC): update_warn = hass.loop.call_later( SLOW_UPDATE_WARNING, self._async_slow_update_warning ) + try: - await coro + if hasattr(self, "async_update"): + await self.async_update() + elif hasattr(self, "update"): + await hass.async_add_executor_job(self.update) + else: + return finally: self._update_staged = False if warning: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a30652fac8d..bc0ba680fc0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ bcrypt==4.0.1 bleak-retry-connector==3.0.2 bleak==0.20.2 bluetooth-adapters==0.15.3 -bluetooth-auto-recovery==1.1.2 +bluetooth-auto-recovery==1.2.0 bluetooth-data-tools==0.4.0 certifi>=2021.5.30 ciso8601==2.3.0 diff --git a/homeassistant/util/language.py b/homeassistant/util/language.py index 8324293e136..615024e059c 100644 --- a/homeassistant/util/language.py +++ b/homeassistant/util/language.py @@ -54,6 +54,20 @@ def is_region(language: str, region: str | None) -> bool: return True +def is_language_match(lang_1: str, lang_2: str) -> bool: + """Return true if two languages are considered the same.""" + if lang_1 == lang_2: + # Exact match + return True + + if {lang_1, lang_2} == {"no", "nb"}: + # no = spoken Norwegian + # nb = written Norwegian (Bokmål) + return True + + return False + + @dataclass class Dialect: """Language with optional region and script/code.""" @@ -71,26 +85,35 @@ class Dialect: # Regions are upper-cased self.region = self.region.upper() - def score(self, dialect: Dialect, country: str | None = None) -> float: + def score( + self, dialect: Dialect, country: str | None = None + ) -> tuple[float, float]: """Return score for match with another dialect where higher is better. Score < 0 indicates a failure to match. """ - if self.language != dialect.language: + if not is_language_match(self.language, dialect.language): # Not a match - return -1 + return (-1, 0) + + is_exact_language = self.language == dialect.language if (self.region is None) and (dialect.region is None): # Weak match with no region constraint - return 1 + # Prefer exact language match + return (2 if is_exact_language else 1, 0) if (self.region is not None) and (dialect.region is not None): if self.region == dialect.region: - # Exact language + region match - return math.inf + # Same language + region match + # Prefer exact language match + return ( + math.inf, + 1 if is_exact_language else 0, + ) # Regions are both set, but don't match - return 0 + return (0, 0) # Generate ordered list of preferred regions pref_regions = list( @@ -113,13 +136,13 @@ class Dialect: # More preferred regions are at the front. # Add 1 to boost above a weak match where no regions are set. - return 1 + (len(pref_regions) - region_idx) + return (1 + (len(pref_regions) - region_idx), 0) except ValueError: # Region was not in preferred list pass # Not a preferred region - return 0 + return (0, 0) @staticmethod def parse(tag: str) -> Dialect: @@ -169,4 +192,4 @@ def matches( ) # Score < 0 is not a match - return [tag for _dialect, score, tag in scored if score >= 0] + return [tag for _dialect, score, tag in scored if score[0] >= 0] diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index aa1b933e0ae..664d6f15650 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -73,8 +73,6 @@ def create_no_verify_ssl_context( https://github.com/aio-libs/aiohttp/blob/33953f110e97eecc707e1402daa8d543f38a189b/aiohttp/connector.py#L911 """ sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - sslcontext.options |= ssl.OP_NO_SSLv2 - sslcontext.options |= ssl.OP_NO_SSLv3 sslcontext.check_hostname = False sslcontext.verify_mode = ssl.CERT_NONE with contextlib.suppress(AttributeError): diff --git a/pyproject.toml b/pyproject.toml index 20a02528aff..2780f467729 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.5.2" +version = "2023.5.3" 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 5e3d4d52d5c..7b6809e9a96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -116,7 +116,7 @@ aio_georss_gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone -aioairzone==0.5.2 +aioairzone==0.5.5 # homeassistant.components.ambient_station aioambient==2023.04.0 @@ -156,7 +156,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.7.3 +aioesphomeapi==13.7.4 # homeassistant.components.flo aioflo==2021.11.0 @@ -223,7 +223,7 @@ aionanoleaf==0.2.1 aionotify==0.2.0 # homeassistant.components.notion -aionotion==2023.05.0 +aionotion==2023.05.4 # homeassistant.components.oncue aiooncue==0.3.4 @@ -300,7 +300,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.2 +aiowebostv==0.3.3 # homeassistant.components.yandex_transport aioymaps==1.2.2 @@ -383,7 +383,7 @@ async-upnp-client==0.33.1 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.3.4 +asyncsleepiq==1.3.5 # homeassistant.components.aten_pe # atenpdu==0.3.2 @@ -428,10 +428,10 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.35.2 +bellows==0.35.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.13.2 +bimmer_connected==0.13.3 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -465,7 +465,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.15.3 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.1.2 +bluetooth-auto-recovery==1.2.0 # homeassistant.components.bluetooth # homeassistant.components.esphome @@ -683,7 +683,7 @@ epsonprinter==0.0.9 esphome-dashboard-api==1.2.3 # homeassistant.components.netgear_lte -eternalegypt==0.0.15 +eternalegypt==0.0.16 # homeassistant.components.eufylife_ble eufylife_ble_client==0.1.7 @@ -1859,7 +1859,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.7.7 +pyoverkiz==1.7.8 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -2242,7 +2242,7 @@ regenmaschine==2022.11.0 renault-api==0.1.13 # homeassistant.components.reolink -reolink-aio==0.5.13 +reolink-aio==0.5.15 # homeassistant.components.python_script restrictedpython==6.0 @@ -2390,7 +2390,7 @@ solax==0.3.0 somfy-mylink-synergy==1.0.6 # homeassistant.components.sonos -sonos-websocket==0.1.0 +sonos-websocket==0.1.1 # homeassistant.components.marytts speak2mary==1.4.0 @@ -2565,7 +2565,7 @@ unifi-discovery==1.1.7 unifiled==0.11 # homeassistant.components.upb -upb_lib==0.5.3 +upb_lib==0.5.4 # homeassistant.components.upcloud upcloud-api==2.0.0 @@ -2582,7 +2582,7 @@ uvcclient==0.11.0 vallox-websocket-api==3.2.1 # homeassistant.components.rdw -vehicle==1.0.0 +vehicle==1.0.1 # homeassistant.components.velbus velbus-aio==2023.2.0 @@ -2600,7 +2600,7 @@ voip-utils==0.0.7 volkszaehler==0.4.0 # homeassistant.components.volvooncall -volvooncall==0.10.2 +volvooncall==0.10.3 # homeassistant.components.verisure vsure==2.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6762e481d02..eba8bdc3f97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -106,7 +106,7 @@ aio_georss_gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone -aioairzone==0.5.2 +aioairzone==0.5.5 # homeassistant.components.ambient_station aioambient==2023.04.0 @@ -146,7 +146,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.7.3 +aioesphomeapi==13.7.4 # homeassistant.components.flo aioflo==2021.11.0 @@ -204,7 +204,7 @@ aiomusiccast==0.14.8 aionanoleaf==0.2.1 # homeassistant.components.notion -aionotion==2023.05.0 +aionotion==2023.05.4 # homeassistant.components.oncue aiooncue==0.3.4 @@ -281,7 +281,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.2 +aiowebostv==0.3.3 # homeassistant.components.yandex_transport aioymaps==1.2.2 @@ -340,7 +340,7 @@ arcam-fmj==1.3.0 async-upnp-client==0.33.1 # homeassistant.components.sleepiq -asyncsleepiq==1.3.4 +asyncsleepiq==1.3.5 # homeassistant.components.aurora auroranoaa==0.0.3 @@ -361,10 +361,10 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.35.2 +bellows==0.35.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.13.2 +bimmer_connected==0.13.3 # homeassistant.components.bluetooth bleak-retry-connector==3.0.2 @@ -385,7 +385,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.15.3 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.1.2 +bluetooth-auto-recovery==1.2.0 # homeassistant.components.bluetooth # homeassistant.components.esphome @@ -1357,7 +1357,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.7.7 +pyoverkiz==1.7.8 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1611,7 +1611,7 @@ regenmaschine==2022.11.0 renault-api==0.1.13 # homeassistant.components.reolink -reolink-aio==0.5.13 +reolink-aio==0.5.15 # homeassistant.components.python_script restrictedpython==6.0 @@ -1714,7 +1714,7 @@ solax==0.3.0 somfy-mylink-synergy==1.0.6 # homeassistant.components.sonos -sonos-websocket==0.1.0 +sonos-websocket==0.1.1 # homeassistant.components.marytts speak2mary==1.4.0 @@ -1841,7 +1841,7 @@ ultraheat-api==0.5.1 unifi-discovery==1.1.7 # homeassistant.components.upb -upb_lib==0.5.3 +upb_lib==0.5.4 # homeassistant.components.upcloud upcloud-api==2.0.0 @@ -1858,7 +1858,7 @@ uvcclient==0.11.0 vallox-websocket-api==3.2.1 # homeassistant.components.rdw -vehicle==1.0.0 +vehicle==1.0.1 # homeassistant.components.velbus velbus-aio==2023.2.0 @@ -1873,7 +1873,7 @@ vilfo-api-client==0.3.2 voip-utils==0.0.7 # homeassistant.components.volvooncall -volvooncall==0.10.2 +volvooncall==0.10.3 # homeassistant.components.verisure vsure==2.6.1 diff --git a/tests/components/airzone/test_binary_sensor.py b/tests/components/airzone/test_binary_sensor.py index 860bc50e93c..8033871f5c3 100644 --- a/tests/components/airzone/test_binary_sensor.py +++ b/tests/components/airzone/test_binary_sensor.py @@ -84,3 +84,9 @@ async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.airzone_2_1_problem") assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dkn_plus_battery_low") + assert state is None + + state = hass.states.get("binary_sensor.dkn_plus_problem") + assert state.state == STATE_OFF diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index 640826bb30f..caf8cfe13bd 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -145,6 +145,24 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP assert state.attributes.get(ATTR_TEMPERATURE) == 19.0 + state = hass.states.get("climate.dkn_plus") + assert state.state == HVACMode.HEAT_COOL + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 21.7 + assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.COOLING + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVACMode.FAN_ONLY, + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.DRY, + HVACMode.HEAT_COOL, + HVACMode.OFF, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 32.2 + assert state.attributes.get(ATTR_MIN_TEMP) == 17.8 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 22.8 + async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: """Test turning on.""" diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index 1e7d335a46f..c72c083039e 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -52,3 +52,9 @@ async def test_airzone_create_sensors( state = hass.states.get("sensor.airzone_2_1_humidity") assert state.state == "62" + + state = hass.states.get("sensor.dkn_plus_temperature") + assert state.state == "21.7" + + state = hass.states.get("sensor.dkn_plus_humidity") + assert state is None diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 6277c077c00..bbbe00a431b 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -7,10 +7,16 @@ from aioairzone.const import ( API_COLD_ANGLE, API_COLD_STAGE, API_COLD_STAGES, + API_COOL_MAX_TEMP, + API_COOL_MIN_TEMP, + API_COOL_SET_POINT, API_DATA, API_ERRORS, API_FLOOR_DEMAND, API_HEAT_ANGLE, + API_HEAT_MAX_TEMP, + API_HEAT_MIN_TEMP, + API_HEAT_SET_POINT, API_HEAT_STAGE, API_HEAT_STAGES, API_HUMIDITY, @@ -25,6 +31,8 @@ from aioairzone.const import ( API_ROOM_TEMP, API_SET_POINT, API_SLEEP, + API_SPEED, + API_SPEEDS, API_SYSTEM_FIRMWARE, API_SYSTEM_ID, API_SYSTEM_TYPE, @@ -216,6 +224,39 @@ HVAC_MOCK = { }, ] }, + { + API_DATA: [ + { + API_SYSTEM_ID: 3, + API_ZONE_ID: 1, + API_NAME: "DKN Plus", + API_ON: 1, + API_COOL_SET_POINT: 73, + API_COOL_MAX_TEMP: 90, + API_COOL_MIN_TEMP: 64, + API_HEAT_SET_POINT: 77, + API_HEAT_MAX_TEMP: 86, + API_HEAT_MIN_TEMP: 50, + API_MAX_TEMP: 90, + API_MIN_TEMP: 64, + API_SET_POINT: 73, + API_ROOM_TEMP: 71, + API_MODES: [4, 2, 3, 5, 7], + API_MODE: 7, + API_SPEEDS: 5, + API_SPEED: 2, + API_COLD_STAGES: 0, + API_COLD_STAGE: 0, + API_HEAT_STAGES: 0, + API_HEAT_STAGE: 0, + API_HUMIDITY: 0, + API_UNITS: 1, + API_ERRORS: [], + API_AIR_DEMAND: 1, + API_FLOOR_DEMAND: 0, + }, + ] + }, ] } diff --git a/tests/components/alexa/test_config.py b/tests/components/alexa/test_config.py new file mode 100644 index 00000000000..9536e3d471b --- /dev/null +++ b/tests/components/alexa/test_config.py @@ -0,0 +1,21 @@ +"""Test config.""" +import asyncio +from unittest.mock import patch + +from homeassistant.core import HomeAssistant + +from .test_common import get_default_config + + +async def test_enable_proactive_mode_in_parallel(hass: HomeAssistant) -> None: + """Test enabling proactive mode does not happen in parallel.""" + config = get_default_config(hass) + + with patch( + "homeassistant.components.alexa.config.async_enable_proactive_mode" + ) as mock_enable_proactive_mode: + await asyncio.gather( + config.async_enable_proactive_mode(), config.async_enable_proactive_mode() + ) + + mock_enable_proactive_mode.assert_awaited_once() diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index fe68bd6547a..67bf2754d11 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -1754,6 +1754,8 @@ async def test_light_strip_zones_not_populated_yet(hass: HomeAssistant) -> None: bulb.power_level = 65535 bulb.color_zones = None bulb.color = [65535, 65535, 65535, 65535] + assert bulb.get_color_zones.calls == [] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( device=bulb ), _patch_device(device=bulb): @@ -1761,6 +1763,14 @@ async def test_light_strip_zones_not_populated_yet(hass: HomeAssistant) -> None: await hass.async_block_till_done() entity_id = "light.my_bulb" + # Make sure we at least try to fetch the first zone + # to ensure we populate the zones from the 503 response + assert len(bulb.get_color_zones.calls) == 3 + # Once to populate the number of zones + assert bulb.get_color_zones.calls[0][1]["start_index"] == 0 + # Again once we know the number of zones + assert bulb.get_color_zones.calls[1][1]["start_index"] == 0 + assert bulb.get_color_zones.calls[2][1]["start_index"] == 8 state = hass.states.get(entity_id) assert state.state == "on" diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index 75eeda70300..81d69158e82 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -3,8 +3,9 @@ from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch -from aionotion.bridge.models import Bridge -from aionotion.sensor.models import Listener, Sensor +from aionotion.bridge.models import BridgeAllResponse +from aionotion.sensor.models import ListenerAllResponse, SensorAllResponse +from aionotion.user.models import UserPreferencesResponse import pytest from homeassistant.components.notion import DOMAIN @@ -27,24 +28,23 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="client") -def client_fixture(data_bridge, data_listener, data_sensor): +def client_fixture(data_bridge, data_listener, data_sensor, data_user_preferences): """Define a fixture for an aionotion client.""" return Mock( bridge=Mock( - async_all=AsyncMock( - return_value=[Bridge.parse_obj(bridge) for bridge in data_bridge] - ) + async_all=AsyncMock(return_value=BridgeAllResponse.parse_obj(data_bridge)) ), sensor=Mock( - async_all=AsyncMock( - return_value=[Sensor.parse_obj(sensor) for sensor in data_sensor] - ), + async_all=AsyncMock(return_value=SensorAllResponse.parse_obj(data_sensor)), async_listeners=AsyncMock( - return_value=[ - Listener.parse_obj(listener) for listener in data_listener - ] + return_value=ListenerAllResponse.parse_obj(data_listener) ), ), + user=Mock( + async_preferences=AsyncMock( + return_value=UserPreferencesResponse.parse_obj(data_user_preferences) + ) + ), ) @@ -83,6 +83,12 @@ def data_sensor_fixture(): return json.loads(load_fixture("sensor_data.json", "notion")) +@pytest.fixture(name="data_user_preferences", scope="package") +def data_user_preferences_fixture(): + """Define user preferences data.""" + return json.loads(load_fixture("user_preferences_data.json", "notion")) + + @pytest.fixture(name="get_client") def get_client_fixture(client): """Define a fixture to mock the async_get_client method.""" diff --git a/tests/components/notion/fixtures/bridge_data.json b/tests/components/notion/fixtures/bridge_data.json index 008967ece86..05bd8859e7e 100644 --- a/tests/components/notion/fixtures/bridge_data.json +++ b/tests/components/notion/fixtures/bridge_data.json @@ -1,50 +1,52 @@ -[ - { - "id": 12345, - "name": "Bridge 1", - "mode": "home", - "hardware_id": "0x0000000000000000", - "hardware_revision": 4, - "firmware_version": { - "silabs": "1.1.2", - "wifi": "0.121.0", - "wifi_app": "3.3.0" +{ + "base_stations": [ + { + "id": 12345, + "name": "Bridge 1", + "mode": "home", + "hardware_id": "0x0000000000000000", + "hardware_revision": 4, + "firmware_version": { + "silabs": "1.1.2", + "wifi": "0.121.0", + "wifi_app": "3.3.0" + }, + "missing_at": null, + "created_at": "2019-06-27T00:18:44.337Z", + "updated_at": "2023-03-19T03:20:16.061Z", + "system_id": 11111, + "firmware": { + "silabs": "1.1.2", + "wifi": "0.121.0", + "wifi_app": "3.3.0" + }, + "links": { + "system": 11111 + } }, - "missing_at": null, - "created_at": "2019-06-27T00:18:44.337Z", - "updated_at": "2023-03-19T03:20:16.061Z", - "system_id": 11111, - "firmware": { - "silabs": "1.1.2", - "wifi": "0.121.0", - "wifi_app": "3.3.0" - }, - "links": { - "system": 11111 + { + "id": 67890, + "name": "Bridge 2", + "mode": "home", + "hardware_id": "0x0000000000000000", + "hardware_revision": 4, + "firmware_version": { + "wifi": "0.121.0", + "wifi_app": "3.3.0", + "silabs": "1.1.2" + }, + "missing_at": null, + "created_at": "2019-04-30T01:43:50.497Z", + "updated_at": "2023-01-02T19:09:58.251Z", + "system_id": 11111, + "firmware": { + "wifi": "0.121.0", + "wifi_app": "3.3.0", + "silabs": "1.1.2" + }, + "links": { + "system": 11111 + } } - }, - { - "id": 67890, - "name": "Bridge 2", - "mode": "home", - "hardware_id": "0x0000000000000000", - "hardware_revision": 4, - "firmware_version": { - "wifi": "0.121.0", - "wifi_app": "3.3.0", - "silabs": "1.1.2" - }, - "missing_at": null, - "created_at": "2019-04-30T01:43:50.497Z", - "updated_at": "2023-01-02T19:09:58.251Z", - "system_id": 11111, - "firmware": { - "wifi": "0.121.0", - "wifi_app": "3.3.0", - "silabs": "1.1.2" - }, - "links": { - "system": 11111 - } - } -] + ] +} diff --git a/tests/components/notion/fixtures/listener_data.json b/tests/components/notion/fixtures/listener_data.json index bd49aab89db..6d59dde76df 100644 --- a/tests/components/notion/fixtures/listener_data.json +++ b/tests/components/notion/fixtures/listener_data.json @@ -1,55 +1,57 @@ -[ - { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "definition_id": 4, - "created_at": "2019-06-28T22:12:49.651Z", - "type": "sensor", - "model_version": "2.1", - "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "status": { - "trigger_value": "no_leak", - "data_received_at": "2022-03-20T08:00:29.763Z" - }, - "status_localized": { - "state": "No Leak", - "description": "Mar 20 at 2:00am" - }, - "insights": { - "primary": { - "origin": { - "type": "Sensor", - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - }, - "value": "no_leak", +{ + "listeners": [ + { + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "definition_id": 4, + "created_at": "2019-06-28T22:12:49.651Z", + "type": "sensor", + "model_version": "2.1", + "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "status": { + "trigger_value": "no_leak", "data_received_at": "2022-03-20T08:00:29.763Z" - } + }, + "status_localized": { + "state": "No Leak", + "description": "Mar 20 at 2:00am" + }, + "insights": { + "primary": { + "origin": { + "type": "Sensor", + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + }, + "value": "no_leak", + "data_received_at": "2022-03-20T08:00:29.763Z" + } + }, + "configuration": {}, + "pro_monitoring_status": "eligible" }, - "configuration": {}, - "pro_monitoring_status": "eligible" - }, - { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "definition_id": 7, - "created_at": "2019-07-10T22:40:48.847Z", - "type": "sensor", - "model_version": "3.1", - "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "status": { - "trigger_value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516Z" - }, - "status_localized": { - "state": "No Sound", - "description": "Jun 28 at 4:12pm" - }, - "insights": { - "primary": { - "origin": {}, - "value": "no_alarm", + { + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "definition_id": 7, + "created_at": "2019-07-10T22:40:48.847Z", + "type": "sensor", + "model_version": "3.1", + "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "status": { + "trigger_value": "no_alarm", "data_received_at": "2019-06-28T22:12:49.516Z" - } - }, - "configuration": {}, - "pro_monitoring_status": "eligible" - } -] + }, + "status_localized": { + "state": "No Sound", + "description": "Jun 28 at 4:12pm" + }, + "insights": { + "primary": { + "origin": {}, + "value": "no_alarm", + "data_received_at": "2019-06-28T22:12:49.516Z" + } + }, + "configuration": {}, + "pro_monitoring_status": "eligible" + } + ] +} diff --git a/tests/components/notion/fixtures/sensor_data.json b/tests/components/notion/fixtures/sensor_data.json index e042daf6ddd..9f0d0fe2e03 100644 --- a/tests/components/notion/fixtures/sensor_data.json +++ b/tests/components/notion/fixtures/sensor_data.json @@ -1,34 +1,36 @@ -[ - { - "id": 123456, - "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "user": { - "id": 12345, - "email": "user@email.com" - }, - "bridge": { - "id": 67890, - "hardware_id": "0x0000000000000000" - }, - "last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "name": "Sensor 1", - "location_id": 123456, - "system_id": 12345, - "hardware_id": "0x0000000000000000", - "hardware_revision": 5, - "firmware_version": "1.1.2", - "device_key": "0x0000000000000000", - "encryption_key": true, - "installed_at": "2019-06-28T22:12:51.209Z", - "calibrated_at": "2023-03-07T19:51:56.838Z", - "last_reported_at": "2023-04-19T18:09:40.479Z", - "missing_at": null, - "updated_at": "2023-03-28T13:33:33.801Z", - "created_at": "2019-06-28T22:12:20.256Z", - "signal_strength": 4, - "firmware": { - "status": "valid" - }, - "surface_type": null - } -] +{ + "sensors": [ + { + "id": 123456, + "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "user": { + "id": 12345, + "email": "user@email.com" + }, + "bridge": { + "id": 67890, + "hardware_id": "0x0000000000000000" + }, + "last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "name": "Sensor 1", + "location_id": 123456, + "system_id": 12345, + "hardware_id": "0x0000000000000000", + "hardware_revision": 5, + "firmware_version": "1.1.2", + "device_key": "0x0000000000000000", + "encryption_key": true, + "installed_at": "2019-06-28T22:12:51.209Z", + "calibrated_at": "2023-03-07T19:51:56.838Z", + "last_reported_at": "2023-04-19T18:09:40.479Z", + "missing_at": null, + "updated_at": "2023-03-28T13:33:33.801Z", + "created_at": "2019-06-28T22:12:20.256Z", + "signal_strength": 4, + "firmware": { + "status": "valid" + }, + "surface_type": null + } + ] +} diff --git a/tests/components/notion/fixtures/user_preferences_data.json b/tests/components/notion/fixtures/user_preferences_data.json new file mode 100644 index 00000000000..6fa603e9d85 --- /dev/null +++ b/tests/components/notion/fixtures/user_preferences_data.json @@ -0,0 +1,10 @@ +{ + "user_preferences": { + "user_id": 12345, + "military_time_enabled": false, + "celsius_enabled": false, + "disconnect_alerts_enabled": true, + "home_away_alerts_enabled": false, + "battery_alerts_enabled": true + } +} diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index 7062778e812..b59b995b404 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -86,14 +86,6 @@ async def test_entry_diagnostics( "device_type": "sensor", "model_version": "3.1", "sensor_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "status": { - "trigger_value": "no_alarm", - "data_received_at": "2019-06-28T22:12:49.516000+00:00", - }, - "status_localized": { - "state": "No Sound", - "description": "Jun 28 at 4:12pm", - }, "insights": { "primary": { "origin": {"type": None, "id": None}, @@ -103,6 +95,14 @@ async def test_entry_diagnostics( }, "configuration": {}, "pro_monitoring_status": "eligible", + "status": { + "trigger_value": "no_alarm", + "data_received_at": "2019-06-28T22:12:49.516000+00:00", + }, + "status_localized": { + "state": "No Sound", + "description": "Jun 28 at 4:12pm", + }, } ], "sensors": [ @@ -131,5 +131,13 @@ async def test_entry_diagnostics( "surface_type": None, } ], + "user_preferences": { + "user_id": REDACTED, + "military_time_enabled": False, + "celsius_enabled": False, + "disconnect_alerts_enabled": True, + "home_away_alerts_enabled": False, + "battery_alerts_enabled": True, + }, }, } diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py index a56e0a477e7..598546a6417 100644 --- a/tests/components/onvif/__init__.py +++ b/tests/components/onvif/__init__.py @@ -101,6 +101,8 @@ def setup_mock_onvif_camera( mock_onvif_camera.create_devicemgmt_service = AsyncMock(return_value=devicemgmt) mock_onvif_camera.create_media_service = AsyncMock(return_value=media_service) mock_onvif_camera.close = AsyncMock(return_value=None) + mock_onvif_camera.xaddrs = {} + mock_onvif_camera.services = {} def mock_constructor( host, diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 21ef1cf3fc2..8187a427be9 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -5,7 +5,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp from homeassistant.components.onvif import DOMAIN, config_flow from homeassistant.config_entries import SOURCE_DHCP -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr @@ -710,6 +710,14 @@ async def test_discovered_by_dhcp_does_not_update_if_no_matching_entry( assert result["reason"] == "no_devices_found" +def _get_schema_default(schema, key_name): + """Iterate schema to find a key.""" + for schema_key in schema: + if schema_key == key_name: + return schema_key.default() + raise KeyError(f"{key_name} not found in schema") + + async def test_form_reauth(hass: HomeAssistant) -> None: """Test reauthenticate.""" entry, _, _ = await setup_onvif_integration(hass) @@ -721,6 +729,10 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" + assert ( + _get_schema_default(result["data_schema"].schema, CONF_USERNAME) + == entry.data[CONF_USERNAME] + ) with patch( "homeassistant.components.onvif.config_flow.get_device" diff --git a/tests/components/onvif/test_diagnostics.py b/tests/components/onvif/test_diagnostics.py index 70dafe960b4..2ab2deb6884 100644 --- a/tests/components/onvif/test_diagnostics.py +++ b/tests/components/onvif/test_diagnostics.py @@ -1,4 +1,6 @@ """Test ONVIF diagnostics.""" +from unittest.mock import ANY + from homeassistant.core import HomeAssistant from . import ( @@ -71,6 +73,8 @@ async def test_diagnostics( "video_source_token": None, } ], + "services": ANY, + "xaddrs": ANY, }, "events": { "pullpoint_manager_state": { diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index cbd5ef379e8..55eb8086842 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -7,6 +7,7 @@ from roborock.containers import ( Consumable, DNDTimer, HomeData, + NetworkInfo, Status, UserData, ) @@ -368,3 +369,7 @@ STATUS = Status.from_dict( ) PROP = DeviceProp(STATUS, DND_TIMER, CLEAN_SUMMARY, CONSUMABLE, CLEAN_RECORD) + +NETWORK_INFO = NetworkInfo( + ip="123.232.12.1", ssid="wifi", mac="ac:cc:cc:cc:cc", bssid="bssid", rssi=90 +) diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 05bf0848475..18d9ee1bafe 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -1,6 +1,8 @@ """Test for Roborock init.""" from unittest.mock import patch +from roborock.exceptions import RoborockTimeout + from homeassistant.components.roborock.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -8,6 +10,7 @@ from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +from tests.components.roborock.mock_data import HOME_DATA, NETWORK_INFO async def test_unload_entry( @@ -38,3 +41,23 @@ async def test_config_entry_not_ready( ): await async_setup_component(hass, DOMAIN, {}) assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_continue_setup_mqtt_disconnect_fail( + hass: HomeAssistant, mock_roborock_entry: MockConfigEntry +): + """Test that if disconnect fails, we still continue setting up.""" + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data", + return_value=HOME_DATA, + ), patch( + "homeassistant.components.roborock.RoborockMqttClient.get_networking", + return_value=NETWORK_INFO, + ), patch( + "homeassistant.components.roborock.RoborockMqttClient.async_disconnect", + side_effect=RoborockTimeout(), + ), patch( + "homeassistant.components.roborock.RoborockDataUpdateCoordinator.async_config_entry_first_refresh" + ): + await async_setup_component(hass, DOMAIN, {}) + assert mock_roborock_entry.state is ConfigEntryState.LOADED diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index bb95860142d..40c402f6f49 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -531,6 +531,41 @@ async def test_async_parallel_updates_with_two(hass: HomeAssistant) -> None: test_lock.release() +async def test_async_parallel_updates_with_one_using_executor( + hass: HomeAssistant, +) -> None: + """Test parallel updates with 1 (sequential) using the executor.""" + test_semaphore = asyncio.Semaphore(1) + locked = [] + + class SyncEntity(entity.Entity): + """Test entity.""" + + def __init__(self, entity_id): + """Initialize sync test entity.""" + self.entity_id = entity_id + self.hass = hass + self.parallel_updates = test_semaphore + + def update(self): + """Test update.""" + locked.append(self.parallel_updates.locked()) + + entities = [SyncEntity(f"sensor.test_{i}") for i in range(3)] + + await asyncio.gather( + *[ + hass.async_create_task( + ent.async_update_ha_state(True), + f"Entity schedule update ha state {ent.entity_id}", + ) + for ent in entities + ] + ) + + assert locked == [True, True, True] + + async def test_async_remove_no_platform(hass: HomeAssistant) -> None: """Test async_remove method when no platform set.""" ent = entity.Entity() diff --git a/tests/util/test_language.py b/tests/util/test_language.py index 70c38a38f00..41f3ef4b301 100644 --- a/tests/util/test_language.py +++ b/tests/util/test_language.py @@ -190,3 +190,39 @@ def test_sr_latn() -> None: "sr-CS", "sr-RS", ] + + +def test_no_nb_same() -> None: + """Test that the no/nb are interchangeable.""" + assert language.matches( + "no", + ["en-US", "en-GB", "nb"], + ) == ["nb"] + assert language.matches( + "nb", + ["en-US", "en-GB", "no"], + ) == ["no"] + + +def test_no_nb_prefer_exact() -> None: + """Test that the exact language is preferred even if an interchangeable language is available.""" + assert language.matches( + "no", + ["en-US", "en-GB", "nb", "no"], + ) == ["no", "nb"] + assert language.matches( + "no", + ["en-US", "en-GB", "no", "nb"], + ) == ["no", "nb"] + + +def test_no_nb_prefer_exact_regions() -> None: + """Test that the exact language/region is preferred.""" + assert language.matches( + "no-AA", + ["en-US", "en-GB", "nb-AA", "no-AA"], + ) == ["no-AA", "nb-AA"] + assert language.matches( + "no-AA", + ["en-US", "en-GB", "no-AA", "nb-AA"], + ) == ["no-AA", "nb-AA"]