diff --git a/homeassistant/components/androidtv_remote/__init__.py b/homeassistant/components/androidtv_remote/__init__.py index bdcf08bb2f6..9299b1ed0b0 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -1,6 +1,7 @@ """The Android TV Remote integration.""" from __future__ import annotations +import asyncio import logging from androidtvremote2 import ( @@ -9,6 +10,7 @@ from androidtvremote2 import ( ConnectionClosed, InvalidAuth, ) +import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform @@ -43,11 +45,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.add_is_available_updated_callback(is_available_updated) try: - await api.async_connect() + async with async_timeout.timeout(5.0): + await api.async_connect() except InvalidAuth as exc: # The Android TV is hard reset or the certificate and key files were deleted. raise ConfigEntryAuthFailed from exc - except (CannotConnect, ConnectionClosed) as exc: + except (CannotConnect, ConnectionClosed, asyncio.TimeoutError) as exc: # The Android TV is network unreachable. Raise exception and let Home Assistant retry # later. If device gets a new IP address the zeroconf flow will update the config. raise ConfigEntryNotReady from exc diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index b0097f607d5..3ef9c0aba62 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -139,7 +139,7 @@ async def async_migrate_unique_id( dev_reg = dr.async_get(hass) old_unique_id = config_entry.unique_id new_unique_id = api.device.mac - new_name = api.device.values["name"] + new_name = api.device.values.get("name") @callback def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index e635b1f7021..54b65c17e60 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["devolo_plc_api"], "quality_scale": "platinum", - "requirements": ["devolo-plc-api==1.3.1"], + "requirements": ["devolo-plc-api==1.3.2"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 4a8a9dec587..0575ac132d4 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.5.35"] + "requirements": ["env-canada==0.5.36"] } diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 4e3d052e6ef..6be1822f90f 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -63,7 +63,10 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): self._attr_native_min_value = static_info.min_value self._attr_native_max_value = static_info.max_value self._attr_native_step = static_info.step - self._attr_native_unit_of_measurement = static_info.unit_of_measurement + # protobuf doesn't support nullable strings so we need to check + # if the string is empty + if unit_of_measurement := static_info.unit_of_measurement: + self._attr_native_unit_of_measurement = unit_of_measurement if mode := static_info.mode: self._attr_mode = NUMBER_MODES.from_esphome(mode) else: diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 3185a5eb536..2e658389e03 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -76,7 +76,10 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): super()._on_static_info_update(static_info) static_info = self._static_info self._attr_force_update = static_info.force_update - self._attr_native_unit_of_measurement = static_info.unit_of_measurement + # protobuf doesn't support nullable strings so we need to check + # if the string is empty + if unit_of_measurement := static_info.unit_of_measurement: + self._attr_native_unit_of_measurement = unit_of_measurement self._attr_device_class = try_parse_enum( SensorDeviceClass, static_info.device_class ) diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 4b3721eed15..d90a9d28662 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyfibaro"], - "requirements": ["pyfibaro==0.7.1"] + "requirements": ["pyfibaro==0.7.2"] } diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 8b24fc912f1..dab8353c773 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -22,7 +22,12 @@ from .const import ( DOMAIN, ) -REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -42,18 +47,12 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm re-authentication with Honeywell.""" errors: dict[str, str] = {} - + assert self.entry is not None if user_input: - assert self.entry is not None - password = user_input[CONF_PASSWORD] - data = { - CONF_USERNAME: self.entry.data[CONF_USERNAME], - CONF_PASSWORD: password, - } - try: await self.is_valid( - username=data[CONF_USERNAME], password=data[CONF_PASSWORD] + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], ) except aiosomecomfort.AuthError: @@ -71,7 +70,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.entry, data={ **self.entry.data, - CONF_PASSWORD: password, + **user_input, }, ) await self.hass.config_entries.async_reload(self.entry.entry_id) @@ -79,7 +78,9 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="reauth_confirm", - data_schema=REAUTH_SCHEMA, + data_schema=self.add_suggested_values_to_schema( + REAUTH_SCHEMA, self.entry.data + ), errors=errors, ) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 16b07e91446..aa07a5248cf 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/honeywell", "iot_class": "cloud_polling", "loggers": ["somecomfort"], - "requirements": ["AIOSomecomfort==0.0.14"] + "requirements": ["AIOSomecomfort==0.0.15"] } diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index bf7f173e647..c3cd21e6b2d 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -13,7 +13,7 @@ from typing import Any from aioimaplib import AUTH, IMAP4_SSL, NONAUTH, SELECTED, AioImapException import async_timeout -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, @@ -54,6 +54,7 @@ _LOGGER = logging.getLogger(__name__) BACKOFF_TIME = 10 EVENT_IMAP = "imap_content" +MAX_ERRORS = 3 MAX_EVENT_DATA_BYTES = 32168 @@ -174,6 +175,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): ) -> None: """Initiate imap client.""" self.imap_client = imap_client + self.auth_errors: int = 0 self._last_message_id: str | None = None self.custom_event_template = None _custom_event_template = entry.data.get(CONF_CUSTOM_EVENT_DATA_TEMPLATE) @@ -315,7 +317,9 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): async def _async_update_data(self) -> int | None: """Update the number of unread emails.""" try: - return await self._async_fetch_number_of_messages() + messages = await self._async_fetch_number_of_messages() + self.auth_errors = 0 + return messages except ( AioImapException, UpdateFailed, @@ -330,8 +334,15 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): self.async_set_update_error(ex) raise ConfigEntryError("Selected mailbox folder is invalid.") from ex except InvalidAuth as ex: - _LOGGER.warning("Username or password incorrect, starting reauthentication") await self._cleanup() + self.auth_errors += 1 + if self.auth_errors <= MAX_ERRORS: + _LOGGER.warning("Authentication failed, retrying") + else: + _LOGGER.warning( + "Username or password incorrect, starting reauthentication" + ) + self.config_entry.async_start_reauth(self.hass) self.async_set_update_error(ex) raise ConfigEntryAuthFailed() from ex @@ -359,27 +370,28 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): async def _async_wait_push_loop(self) -> None: """Wait for data push from server.""" + cleanup = False while True: try: number_of_messages = await self._async_fetch_number_of_messages() except InvalidAuth as ex: + self.auth_errors += 1 await self._cleanup() - _LOGGER.warning( - "Username or password incorrect, starting reauthentication" - ) - self.config_entry.async_start_reauth(self.hass) + if self.auth_errors <= MAX_ERRORS: + _LOGGER.warning("Authentication failed, retrying") + else: + _LOGGER.warning( + "Username or password incorrect, starting reauthentication" + ) + self.config_entry.async_start_reauth(self.hass) self.async_set_update_error(ex) await asyncio.sleep(BACKOFF_TIME) except InvalidFolder as ex: _LOGGER.warning("Selected mailbox folder is invalid") await self._cleanup() - self.config_entry.async_set_state( - self.hass, - ConfigEntryState.SETUP_ERROR, - "Selected mailbox folder is invalid.", - ) self.async_set_update_error(ex) await asyncio.sleep(BACKOFF_TIME) + continue except ( UpdateFailed, AioImapException, @@ -390,6 +402,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): await asyncio.sleep(BACKOFF_TIME) continue else: + self.auth_errors = 0 self.async_set_updated_data(number_of_messages) try: idle: asyncio.Future = await self.imap_client.idle_start() @@ -398,6 +411,10 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): async with async_timeout.timeout(10): await idle + # From python 3.11 asyncio.TimeoutError is an alias of TimeoutError + except asyncio.CancelledError as ex: + cleanup = True + raise asyncio.CancelledError from ex except (AioImapException, asyncio.TimeoutError): _LOGGER.debug( "Lost %s (will attempt to reconnect after %s s)", @@ -406,6 +423,9 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): ) await self._cleanup() await asyncio.sleep(BACKOFF_TIME) + finally: + if cleanup: + await self._cleanup() async def shutdown(self, *_: Any) -> None: """Close resources.""" diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 01f77cb2d38..dd29d02d655 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pymazda"], "quality_scale": "platinum", - "requirements": ["pymazda==0.3.9"] + "requirements": ["pymazda==0.3.10"] } diff --git a/homeassistant/components/mystrom/__init__.py b/homeassistant/components/mystrom/__init__.py index 972db00e476..3166c05db19 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: info.setdefault("type", 101) device_type = info["type"] - if device_type in [101, 106, 107]: + if device_type in [101, 106, 107, 120]: device = _get_mystrom_switch(host) platforms = PLATFORMS_SWITCH await _async_get_device_state(device, info["ip"]) @@ -86,7 +86,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" device_type = hass.data[DOMAIN][entry.entry_id].info["type"] platforms = [] - if device_type in [101, 106, 107]: + if device_type in [101, 106, 107, 120]: platforms.extend(PLATFORMS_SWITCH) elif device_type in [102, 105]: platforms.extend(PLATFORMS_BULB) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index a524d8ea519..358cbbf5c83 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -387,8 +387,12 @@ class ONVIFDevice: "WSPullPointSupport" ) LOGGER.debug("%s: WSPullPointSupport: %s", self.name, pull_point_support) + # Even if the camera claims it does not support PullPoint, try anyway + # since at least some AXIS and Bosch models do. The reverse is also + # true where some cameras claim they support PullPoint but don't so + # the only way to know is to try. return await self.events.async_start( - pull_point_support is not False, + True, self.config_entry.options.get( CONF_ENABLE_WEBHOOKS, DEFAULT_ENABLE_WEBHOOKS ), diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index e92e80a9a68..d03073dcfd3 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==3.1.9", "WSDiscovery==2.0.0"] + "requirements": ["onvif-zeep-async==3.1.12", "WSDiscovery==2.0.0"] } diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index 67c8412102d..9fa38cedbe8 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -50,8 +50,8 @@ async def _title(hass: HomeAssistant, discovery_info: HassioServiceInfo) -> str: addon_info = await async_get_addon_info(hass, discovery_info.slug) device = addon_info.get("options", {}).get("device") - if _is_yellow(hass) and device == "/dev/TTYAMA1": - return "Home Assistant Yellow" + if _is_yellow(hass) and device == "/dev/ttyAMA1": + return f"Home Assistant Yellow ({discovery_info.name})" if device and "SkyConnect" in device: return "Home Assistant SkyConnect" @@ -130,6 +130,11 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): url = f"http://{config['host']}:{config['port']}" config_entry_data = {"url": url} + if self._async_in_progress(include_uninitialized=True): + # We currently don't handle multiple config entries, abort if hassio + # discovers multiple addons with otbr support + return self.async_abort(reason="single_instance_allowed") + if current_entries := self._async_current_entries(): for current_entry in current_entries: if current_entry.source != SOURCE_HASSIO: diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index a44cfb3ce13..986e89783d7 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "iot_class": "local_polling", "loggers": ["pyrainbird"], - "requirements": ["pyrainbird==2.1.0"] + "requirements": ["pyrainbird==3.0.0"] } diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 0cf6db4ae81..5f6aa63ce2f 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.30.0"] + "requirements": ["python-roborock==0.30.1"] } diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index ee9c946268f..cce72dfaae6 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -151,9 +151,10 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): @async_handle_api_call async def async_turn_on_timer(self, key: str, value: bool) -> bool: """Make service call to api for setting timer.""" + new_state = not self.device_data.device_on data = { "minutesFromNow": 60, - "acState": {**self.device_data.ac_states, "on": value}, + "acState": {**self.device_data.ac_states, "on": new_state}, } result = await self._client.async_set_timer(self._device_id, data) return bool(result.get("status") == "success") diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index ef2ecc7aa23..c59150266d9 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -61,6 +61,8 @@ ENTITY_DESCRIPTION_ALARM = SIAAlarmControlPanelEntityDescription( "OS": STATE_ALARM_DISARMED, "NC": STATE_ALARM_ARMED_NIGHT, "NL": STATE_ALARM_ARMED_NIGHT, + "NE": STATE_ALARM_ARMED_CUSTOM_BYPASS, + "NF": STATE_ALARM_ARMED_CUSTOM_BYPASS, "BR": PREVIOUS_STATE, "NP": PREVIOUS_STATE, "NO": PREVIOUS_STATE, diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 4913d76c0c9..b895be83f2e 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -127,6 +127,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_name = None def __init__(self, simplisafe: SimpliSafe, system: SystemType) -> None: """Initialize the SimpliSafe alarm.""" diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index d31dc5da282..34c0ea5ea95 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -111,7 +111,6 @@ class BatteryBinarySensor(SimpliSafeEntity, BinarySensorEntity): """Initialize.""" super().__init__(simplisafe, system, device=device) - self._attr_name = "Battery" self._attr_unique_id = f"{super().unique_id}-battery" self._device: DeviceV3 diff --git a/homeassistant/components/simplisafe/button.py b/homeassistant/components/simplisafe/button.py index d8da8bc7592..bd60c040f56 100644 --- a/homeassistant/components/simplisafe/button.py +++ b/homeassistant/components/simplisafe/button.py @@ -44,7 +44,7 @@ async def _async_clear_notifications(system: System) -> None: BUTTON_DESCRIPTIONS = ( SimpliSafeButtonDescription( key=BUTTON_KIND_CLEAR_NOTIFICATIONS, - name="Clear notifications", + translation_key=BUTTON_KIND_CLEAR_NOTIFICATIONS, push_action=_async_clear_notifications, ), ) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 618c21566f7..4f230442f85 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -29,5 +29,12 @@ } } } + }, + "entity": { + "button": { + "clear_notifications": { + "name": "Clear notifications" + } + } } } diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index d2d0dba6773..5d7e29c1312 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -62,7 +62,11 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: # Must have one of the min_required if any(capability in capabilities for capability in min_required): # Return all capabilities supported/consumed - return min_required + [Capability.battery, Capability.switch_level] + return min_required + [ + Capability.battery, + Capability.switch_level, + Capability.window_shade_level, + ] return None @@ -74,12 +78,16 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): """Initialize the cover class.""" super().__init__(device) self._device_class = None + self._current_cover_position = None self._state = None self._state_attrs = None self._attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE ) - if Capability.switch_level in device.capabilities: + if ( + Capability.switch_level in device.capabilities + or Capability.window_shade_level in device.capabilities + ): self._attr_supported_features |= CoverEntityFeature.SET_POSITION async def async_close_cover(self, **kwargs: Any) -> None: @@ -103,7 +111,12 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): if not self.supported_features & CoverEntityFeature.SET_POSITION: return # Do not set_status=True as device will report progress. - await self._device.set_level(kwargs[ATTR_POSITION], 0) + if Capability.window_shade_level in self._device.capabilities: + await self._device.set_window_shade_level( + kwargs[ATTR_POSITION], set_status=False + ) + else: + await self._device.set_level(kwargs[ATTR_POSITION], set_status=False) async def async_update(self) -> None: """Update the attrs of the cover.""" @@ -117,6 +130,11 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): self._device_class = CoverDeviceClass.GARAGE self._state = VALUE_TO_STATE.get(self._device.status.door) + if Capability.window_shade_level in self._device.capabilities: + self._current_cover_position = self._device.status.shade_level + elif Capability.switch_level in self._device.capabilities: + self._current_cover_position = self._device.status.level + self._state_attrs = {} battery = self._device.status.attributes[Attribute.battery].value if battery is not None: @@ -142,9 +160,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): @property def current_cover_position(self) -> int | None: """Return current position of cover.""" - if not self.supported_features & CoverEntityFeature.SET_POSITION: - return None - return self._device.status.level + return self._current_cover_position @property def device_class(self) -> CoverDeviceClass | None: diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 29eb681dc4d..89e5071051c 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "documentation": "https://www.home-assistant.io/integrations/smartthings", "iot_class": "cloud_push", "loggers": ["httpsig", "pysmartapp", "pysmartthings"], - "requirements": ["pysmartapp==0.3.3", "pysmartthings==0.7.6"] + "requirements": ["pysmartapp==0.3.5", "pysmartthings==0.7.8"] } diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index e448fe066c4..4bc9bb24835 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -42,11 +42,12 @@ from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries from homeassistant.components import network from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, __version__ as current_version, ) -from homeassistant.core import HomeAssistant, callback as core_callback +from homeassistant.core import Event, HomeAssistant, callback as core_callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import config_validation as cv, discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -728,15 +729,18 @@ class Server: async def async_start(self) -> None: """Start the server.""" - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) - await self._async_start_upnp_servers() + bus = self.hass.bus + bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, self._async_start_upnp_servers + ) async def _async_get_instance_udn(self) -> str: """Get Unique Device Name for this instance.""" instance_id = await async_get_instance_id(self.hass) return f"uuid:{instance_id[0:8]}-{instance_id[8:12]}-{instance_id[12:16]}-{instance_id[16:20]}-{instance_id[20:32]}".upper() - async def _async_start_upnp_servers(self) -> None: + async def _async_start_upnp_servers(self, event: Event) -> None: """Start the UPnP/SSDP servers.""" # Update UDN with our instance UDN. udn = await self._async_get_instance_udn() diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index d3920d3f0e4..1d074bba9c2 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -36,6 +36,7 @@ class StookalertBinarySensor(BinarySensorEntity): _attr_attribution = "Data provided by rivm.nl" _attr_device_class = BinarySensorDeviceClass.SAFETY _attr_has_entity_name = True + _attr_name = None def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: """Initialize a Stookalert device.""" diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 5ec27a1768c..7276e7a0d9b 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -151,7 +151,7 @@ def find_moov(mp4_io: BufferedIOBase) -> int: while 1: mp4_io.seek(index) box_header = mp4_io.read(8) - if len(box_header) != 8: + if len(box_header) != 8 or box_header[0:4] == b"\x00\x00\x00\x00": raise HomeAssistantError("moov atom not found") if box_header[4:8] == b"moov": return index diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 8dc04e7da86..7f0866ce62e 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -205,7 +205,6 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { key="process", name="Process", icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, mandatory_arg=True, ), "processor_use": SysMonitorSensorEntityDescription( diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 672bd899962..ecdefd36b2a 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -8,7 +8,7 @@ "requirements": [ "tensorflow==2.5.0", "tf-models-official==2.5.0", - "pycocotools==2.0.1", + "pycocotools==2.0.6", "numpy==1.23.2", "Pillow==9.5.0" ] diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 34eee944114..b43dabbba80 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -84,6 +84,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity): """Representation of the fan.""" _attr_has_entity_name = True + _attr_name = None _attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED def __init__( diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 4f3fcbf9c87..479c84d238c 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vallox", "iot_class": "local_polling", "loggers": ["vallox_websocket_api"], - "requirements": ["vallox-websocket-api==3.2.1"] + "requirements": ["vallox-websocket-api==3.3.0"] } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a52b869b830..7b8401ec8b5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -694,7 +694,7 @@ class ConfigEntry: if self._on_unload is not None: while self._on_unload: if job := self._on_unload.pop()(): - self._tasks.add(hass.async_create_task(job)) + self.async_create_task(hass, job) if not self._tasks and not self._background_tasks: return diff --git a/homeassistant/const.py b/homeassistant/const.py index 30a7fc37c9e..e9b7abd5200 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 = 7 -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/package_constraints.txt b/homeassistant/package_constraints.txt index 71ca8fc4c3e..b08aa7246db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ aiodiscover==1.4.16 aiohttp-cors==0.7.0 -aiohttp==3.8.4 +aiohttp==3.8.5 astral==2.2 async-timeout==4.0.2 async-upnp-client==0.33.2 @@ -42,7 +42,7 @@ pyserial==3.5 python-slugify==4.0.1 PyTurboJPEG==1.6.7 pyudev==0.23.2 -PyYAML==6.0 +PyYAML==6.0.1 requests==2.31.0 scapy==2.5.0 SQLAlchemy==2.0.15 diff --git a/pyproject.toml b/pyproject.toml index a902941213d..42b857c4401 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.2" +version = "2023.7.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -24,7 +24,7 @@ classifiers = [ ] requires-python = ">=3.10.0" dependencies = [ - "aiohttp==3.8.4", + "aiohttp==3.8.5", "astral==2.2", "async-timeout==4.0.2", "attrs==22.2.0", @@ -48,7 +48,7 @@ dependencies = [ "orjson==3.9.1", "pip>=21.3.1,<23.2", "python-slugify==4.0.1", - "PyYAML==6.0", + "PyYAML==6.0.1", "requests==2.31.0", "typing_extensions>=4.6.3,<5.0", "ulid-transform==0.7.2", diff --git a/requirements.txt b/requirements.txt index f4f2608b597..5063ab16a59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.8.4 +aiohttp==3.8.5 astral==2.2 async-timeout==4.0.2 attrs==22.2.0 @@ -21,7 +21,7 @@ pyOpenSSL==23.2.0 orjson==3.9.1 pip>=21.3.1,<23.2 python-slugify==4.0.1 -PyYAML==6.0 +PyYAML==6.0.1 requests==2.31.0 typing_extensions>=4.6.3,<5.0 ulid-transform==0.7.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5c0f16dae97..6d6428f614c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ AEMET-OpenData==0.2.2 AIOAladdinConnect==0.1.56 # homeassistant.components.honeywell -AIOSomecomfort==0.0.14 +AIOSomecomfort==0.0.15 # homeassistant.components.adax Adax-local==0.1.5 @@ -658,7 +658,7 @@ denonavr==0.11.2 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.3.1 +devolo-plc-api==1.3.2 # homeassistant.components.directv directv==0.4.0 @@ -730,7 +730,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.5.35 +env-canada==0.5.36 # homeassistant.components.enphase_envoy envoy-reader==0.20.1 @@ -1327,7 +1327,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==3.1.9 +onvif-zeep-async==3.1.12 # homeassistant.components.opengarage open-garage==0.2.0 @@ -1600,7 +1600,7 @@ pycketcasts==1.0.1 pycmus==0.1.1 # homeassistant.components.tensorflow -pycocotools==2.0.1 +# pycocotools==2.0.6 # homeassistant.components.comfoconnect pycomfoconnect==0.5.1 @@ -1666,7 +1666,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.1 +pyfibaro==0.7.2 # homeassistant.components.fido pyfido==2.1.2 @@ -1810,7 +1810,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.9 +pymazda==0.3.10 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 @@ -1938,7 +1938,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==2.1.0 +pyrainbird==3.0.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 @@ -2000,10 +2000,10 @@ pysma==0.7.3 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartapp==0.3.3 +pysmartapp==0.3.5 # homeassistant.components.smartthings -pysmartthings==0.7.6 +pysmartthings==0.7.8 # homeassistant.components.edl21 pysml==0.0.12 @@ -2139,7 +2139,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.30.0 +python-roborock==0.30.1 # homeassistant.components.smarttub python-smarttub==0.0.33 @@ -2599,7 +2599,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==3.2.1 +vallox-websocket-api==3.3.0 # homeassistant.components.rdw vehicle==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c50f874cf4..4ad65cdccfd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.2.2 AIOAladdinConnect==0.1.56 # homeassistant.components.honeywell -AIOSomecomfort==0.0.14 +AIOSomecomfort==0.0.15 # homeassistant.components.adax Adax-local==0.1.5 @@ -532,7 +532,7 @@ denonavr==0.11.2 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.3.1 +devolo-plc-api==1.3.2 # homeassistant.components.directv directv==0.4.0 @@ -583,7 +583,7 @@ energyzero==0.4.1 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.5.35 +env-canada==0.5.36 # homeassistant.components.enphase_envoy envoy-reader==0.20.1 @@ -1011,7 +1011,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==3.1.9 +onvif-zeep-async==3.1.12 # homeassistant.components.opengarage open-garage==0.2.0 @@ -1227,7 +1227,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.1 +pyfibaro==0.7.2 # homeassistant.components.fido pyfido==2.1.2 @@ -1338,7 +1338,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.9 +pymazda==0.3.10 # homeassistant.components.melcloud pymelcloud==2.5.8 @@ -1439,7 +1439,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.rainbird -pyrainbird==2.1.0 +pyrainbird==3.0.0 # homeassistant.components.risco pyrisco==0.5.7 @@ -1486,10 +1486,10 @@ pysma==0.7.3 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartapp==0.3.3 +pysmartapp==0.3.5 # homeassistant.components.smartthings -pysmartthings==0.7.6 +pysmartthings==0.7.8 # homeassistant.components.edl21 pysml==0.0.12 @@ -1565,7 +1565,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.30.0 +python-roborock==0.30.1 # homeassistant.components.smarttub python-smarttub==0.0.33 @@ -1899,7 +1899,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==3.2.1 +vallox-websocket-api==3.3.0 # homeassistant.components.rdw vehicle==1.0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 0bbbd97c926..c211a0fca81 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -33,6 +33,7 @@ COMMENT_REQUIREMENTS = ( "face-recognition", "opencv-python-headless", "pybluez", + "pycocotools", "pycups", "python-eq3bt", "python-gammu", diff --git a/tests/components/esphome/test_number.py b/tests/components/esphome/test_number.py index 8157c5f5c3d..3af94cba39d 100644 --- a/tests/components/esphome/test_number.py +++ b/tests/components/esphome/test_number.py @@ -15,7 +15,7 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -89,3 +89,36 @@ async def test_generic_number_nan( state = hass.states.get("number.test_my_number") assert state is not None assert state.state == STATE_UNKNOWN + + +async def test_generic_number_with_unit_of_measurement_as_empty_string( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic number entity with nan state.""" + entity_info = [ + NumberInfo( + object_id="mynumber", + key=1, + name="my number", + unique_id="my_number", + max_value=100, + min_value=0, + step=1, + unit_of_measurement="", + mode=ESPHomeNumberMode.SLIDER, + ) + ] + states = [NumberState(key=1, state=42)] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("number.test_my_number") + assert state is not None + assert state.state == "42" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index 8f4eb0f9513..27644617a7a 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -13,7 +13,7 @@ from aioesphomeapi import ( ) from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass -from homeassistant.const import ATTR_ICON, STATE_UNKNOWN +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -275,3 +275,30 @@ async def test_generic_text_sensor( state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "i am a teapot" + + +async def test_generic_numeric_sensor_empty_string_uom( + hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry +) -> None: + """Test a generic numeric sensor that has an empty string as the uom.""" + entity_info = [ + SensorInfo( + object_id="mysensor", + key=1, + name="my sensor", + unique_id="my_sensor", + unit_of_measurement="", + ) + ] + states = [SensorState(key=1, state=123, missing_state=False)] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("sensor.test_my_sensor") + assert state is not None + assert state.state == "123" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index a416f030a05..25ffa0a6093 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -156,14 +156,14 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, ) await hass.async_block_till_done() assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { - CONF_USERNAME: "test-username", + CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password", } @@ -200,7 +200,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, ) await hass.async_block_till_done() @@ -246,7 +246,7 @@ async def test_reauth_flow_connnection_error( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, ) await hass.async_block_till_done() diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index ff949423614..055f8fd82bc 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -228,6 +228,48 @@ async def test_initial_invalid_folder_error( assert (state is not None) == success +@patch("homeassistant.components.imap.coordinator.MAX_ERRORS", 1) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +async def test_late_authentication_retry( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_imap_protocol: MagicMock, +) -> None: + """Test retrying authentication after a search was failed.""" + + # Mock an error in waiting for a pushed update + mock_imap_protocol.wait_server_push.side_effect = AioImapException( + "Something went wrong" + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + # Mock that the search fails, this will trigger + # that the connection will be restarted + # Then fail selecting the folder + mock_imap_protocol.search.return_value = Response(*BAD_RESPONSE) + mock_imap_protocol.login.side_effect = Response(*BAD_RESPONSE) + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + assert "Authentication failed, retrying" in caplog.text + + # we still should have an entity with an unavailable state + state = hass.states.get("sensor.imap_email_email_com") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +@patch("homeassistant.components.imap.coordinator.MAX_ERRORS", 0) @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) async def test_late_authentication_error( hass: HomeAssistant, diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 80011b47915..4100a270e0a 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -68,7 +68,7 @@ async def test_init_switch_and_unload( (110, "sensor", ConfigEntryState.SETUP_ERROR, True), (113, "switch", ConfigEntryState.SETUP_ERROR, True), (118, "button", ConfigEntryState.SETUP_ERROR, True), - (120, "switch", ConfigEntryState.SETUP_ERROR, True), + (120, "switch", ConfigEntryState.LOADED, False), ], ) async def test_init_bulb( diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index b6cb0df78cd..2659f8d151d 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -23,6 +23,12 @@ HASSIO_DATA = hassio.HassioServiceInfo( slug="otbr", uuid="12345", ) +HASSIO_DATA_2 = hassio.HassioServiceInfo( + config={"host": "core-silabs-multiprotocol_2", "port": 8082}, + name="Silicon Labs Multiprotocol", + slug="other_addon", + uuid="23456", +) @pytest.fixture(name="addon_info") @@ -234,7 +240,7 @@ async def test_hassio_discovery_flow_yellow( addon_info.return_value = { "available": True, "hostname": None, - "options": {"device": "/dev/TTYAMA1"}, + "options": {"device": "/dev/ttyAMA1"}, "state": None, "update_available": False, "version": None, @@ -255,7 +261,7 @@ async def test_hassio_discovery_flow_yellow( } assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Home Assistant Yellow" + assert result["title"] == "Home Assistant Yellow (Silicon Labs Multiprotocol)" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -263,7 +269,7 @@ async def test_hassio_discovery_flow_yellow( config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} - assert config_entry.title == "Home Assistant Yellow" + assert config_entry.title == "Home Assistant Yellow (Silicon Labs Multiprotocol)" assert config_entry.unique_id == HASSIO_DATA.uuid @@ -313,6 +319,80 @@ async def test_hassio_discovery_flow_sky_connect( assert config_entry.unique_id == HASSIO_DATA.uuid +async def test_hassio_discovery_flow_2x_addons( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info +) -> None: + """Test the hassio discovery flow when the user has 2 addons with otbr support.""" + url1 = "http://core-silabs-multiprotocol:8081" + url2 = "http://core-silabs-multiprotocol_2:8081" + aioclient_mock.get(f"{url1}/node/dataset/active", text="aa") + aioclient_mock.get(f"{url2}/node/dataset/active", text="bb") + + async def _addon_info(hass, slug): + await asyncio.sleep(0) + if slug == "otbr": + return { + "available": True, + "hostname": None, + "options": { + "device": ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port0" + ) + }, + "state": None, + "update_available": False, + "version": None, + } + return { + "available": True, + "hostname": None, + "options": { + "device": ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port1" + ) + }, + "state": None, + "update_available": False, + "version": None, + } + + addon_info.side_effect = _addon_info + + with patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + results = await asyncio.gather( + hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA + ), + hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA_2 + ), + ) + + expected_data = { + "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", + } + + assert results[0]["type"] == FlowResultType.CREATE_ENTRY + assert results[0]["title"] == "Home Assistant SkyConnect" + assert results[0]["data"] == expected_data + assert results[0]["options"] == {} + assert results[1]["type"] == FlowResultType.ABORT + assert results[1]["reason"] == "single_instance_allowed" + assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] + assert config_entry.data == expected_data + assert config_entry.options == {} + assert config_entry.title == "Home Assistant SkyConnect" + assert config_entry.unique_id == HASSIO_DATA.uuid + + async def test_hassio_discovery_flow_router_not_setup( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info ) -> None: diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 715f26beaa7..4e637450fec 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -103,8 +103,10 @@ async def test_close(hass: HomeAssistant, device_factory) -> None: assert state.state == STATE_CLOSING -async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: - """Test the cover sets to the specific position.""" +async def test_set_cover_position_switch_level( + hass: HomeAssistant, device_factory +) -> None: + """Test the cover sets to the specific position for legacy devices that use Capability.switch_level.""" # Arrange device = device_factory( "Shade", @@ -130,6 +132,37 @@ async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: assert device._api.post_device_command.call_count == 1 # type: ignore +async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: + """Test the cover sets to the specific position.""" + # Arrange + device = device_factory( + "Shade", + [Capability.window_shade, Capability.battery, Capability.window_shade_level], + { + Attribute.window_shade: "opening", + Attribute.battery: 95, + Attribute.shade_level: 10, + }, + ) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) + # Act + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50, "entity_id": "all"}, + blocking=True, + ) + + state = hass.states.get("cover.shade") + # Result of call does not update state + assert state.state == STATE_OPENING + assert state.attributes[ATTR_BATTERY_LEVEL] == 95 + assert state.attributes[ATTR_CURRENT_POSITION] == 10 + # Ensure API called + + assert device._api.post_device_command.call_count == 1 # type: ignore + + async def test_set_cover_position_unsupported( hass: HomeAssistant, device_factory ) -> None: diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index a80b9f48798..ed5241a42ad 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -742,6 +742,8 @@ async def test_bind_failure_skips_adapter( SsdpListener.async_start = _async_start UpnpServer.async_start = _async_start await init_ssdp_component(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() assert "Failed to setup listener for" in caplog.text diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 0dc67c37403..e0152190d90 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -245,7 +245,7 @@ class FakePyAvBuffer: # Forward to appropriate FakeStream packet.stream.mux(packet) # Make new init/part data available to the worker - self.memory_file.write(b"\x00\x00\x00\x00moov") + self.memory_file.write(b"\x00\x00\x00\x08moov") def close(self): """Close the buffer."""