diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 67a54906e92..486651e2058 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -168,28 +168,28 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Leak detector battery 1", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK2, name="Leak detector battery 2", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK3, name="Leak detector battery 3", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK4, name="Leak detector battery 4", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM1, @@ -273,7 +273,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Lightning detector battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_LEAK1, diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index cf00616b65f..a74f3f28072 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.8"], + "requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.12"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 1523f41bf1f..5d0a8edabd9 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -216,13 +216,7 @@ class BluetoothManager: if address in seen: continue seen.add(address) - for domain in self._integration_matcher.match_domains(service_info): - discovery_flow.async_create_flow( - self.hass, - domain, - {"source": config_entries.SOURCE_BLUETOOTH}, - service_info, - ) + self._async_trigger_matching_discovery(service_info) @hass_callback def async_stop(self, event: Event) -> None: @@ -649,10 +643,27 @@ class BluetoothManager: """Return the last service info for an address.""" return self._get_history_by_type(connectable).get(address) + def _async_trigger_matching_discovery( + self, service_info: BluetoothServiceInfoBleak + ) -> None: + """Trigger discovery for matching domains.""" + for domain in self._integration_matcher.match_domains(service_info): + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) + @hass_callback def async_rediscover_address(self, address: str) -> None: """Trigger discovery of devices which have already been seen.""" self._integration_matcher.async_clear_address(address) + if service_info := self._connectable_history.get(address): + self._async_trigger_matching_discovery(service_info) + return + if service_info := self._all_history.get(address): + self._async_trigger_matching_discovery(service_info) def _get_scanners_by_type(self, connectable: bool) -> list[BaseHaScanner]: """Return the scanners by type.""" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 2ae066266e9..34bc5befea5 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.3"], + "requirements": ["aioesphomeapi==13.3.1", "esphome-dashboard-api==1.2.3"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 088678b6c1a..6c379832ced 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -137,7 +137,7 @@ async def async_setup_entry( def calc_min( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate min value.""" val: float | None = None entity_id: str | None = None @@ -153,7 +153,7 @@ def calc_min( def calc_max( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate max value.""" val: float | None = None entity_id: str | None = None @@ -169,7 +169,7 @@ def calc_max( def calc_mean( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate mean value.""" result = (sensor_value for _, sensor_value, _ in sensor_values) @@ -179,7 +179,7 @@ def calc_mean( def calc_median( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate median value.""" result = (sensor_value for _, sensor_value, _ in sensor_values) @@ -189,10 +189,11 @@ def calc_median( def calc_last( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate last value.""" last_updated: datetime | None = None last_entity_id: str | None = None + last: float | None = None for entity_id, state_f, state in sensor_values: if last_updated is None or state.last_updated > last_updated: last_updated = state.last_updated @@ -227,7 +228,9 @@ def calc_sum( CALC_TYPES: dict[ str, - Callable[[list[tuple[str, float, State]]], tuple[dict[str, str | None], float]], + Callable[ + [list[tuple[str, float, State]]], tuple[dict[str, str | None], float | None] + ], ] = { "min": calc_min, "max": calc_max, diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 146e2c2babc..cbdf909001a 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -7,7 +7,7 @@ from functools import wraps import logging from typing import Any, Concatenate, ParamSpec, TypeVar -import aiohttp.client_exceptions +import httpx from iaqualink.client import AqualinkClient from iaqualink.device import ( AqualinkBinarySensor, @@ -77,10 +77,7 @@ async def async_setup_entry( # noqa: C901 _LOGGER.error("Failed to login: %s", login_exception) await aqualink.close() return False - except ( - asyncio.TimeoutError, - aiohttp.client_exceptions.ClientConnectorError, - ) as aio_exception: + except (asyncio.TimeoutError, httpx.HTTPError) as aio_exception: await aqualink.close() raise ConfigEntryNotReady( f"Error while attempting login: {aio_exception}" @@ -149,7 +146,7 @@ async def async_setup_entry( # noqa: C901 try: await system.update() - except AqualinkServiceException as svc_exception: + except (AqualinkServiceException, httpx.HTTPError) as svc_exception: if prev is not None: _LOGGER.warning( "Failed to refresh system %s state: %s", diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index 3b3a99cac6e..0dfc60f2fee 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +import httpx from iaqualink.client import AqualinkClient from iaqualink.exception import ( AqualinkServiceException, @@ -42,7 +43,7 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): pass except AqualinkServiceUnauthorizedException: errors["base"] = "invalid_auth" - except AqualinkServiceException: + except (AqualinkServiceException, httpx.HTTPError): errors["base"] = "cannot_connect" else: return self.async_create_entry(title=username, data=user_input) diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 36dca71e957..7f250f63922 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==3.0.5"], + "requirements": ["pyipma==3.0.6"], "codeowners": ["@dgomes", "@abmantis"], "iot_class": "cloud_polling", "loggers": ["geopy", "pyipma"] diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index f4247f97ecf..a12809e3af7 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", "requirements": [ - "aiolifx==0.8.7", + "aiolifx==0.8.9", "aiolifx_effects==0.3.1", "aiolifx_themes==0.4.0" ], diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 04b986e41ba..ca8246577fd 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -5,7 +5,11 @@ from datetime import datetime import logging from typing import Any -from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + RestoreSensor, + SensorEntity, +) from homeassistant.const import ( CONF_NAME, CONF_SENSORS, @@ -14,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -53,7 +56,7 @@ async def async_setup_platform( async_add_entities(sensors) -class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): +class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): """Modbus register sensor.""" def __init__( @@ -90,8 +93,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() - if state := await self.async_get_last_state(): - self._attr_native_value = state.state + state = await self.async_get_last_sensor_data() + if state: + self._attr_native_value = state.native_value async def async_update(self, now: datetime | None = None) -> None: """Update the state of the sensor.""" @@ -135,7 +139,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): class SlaveSensor( CoordinatorEntity[DataUpdateCoordinator[list[int] | None]], - RestoreEntity, + RestoreSensor, SensorEntity, ): """Modbus slave register sensor.""" diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 9024d0510dd..024c2d9d6d7 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -135,6 +135,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) + if ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp: + return self.async_abort(reason="no_serial") + await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) self._abort_if_unique_id_configured(updates=updated_data) diff --git a/homeassistant/components/netgear/strings.json b/homeassistant/components/netgear/strings.json index 3585d1e613b..d58c4878f65 100644 --- a/homeassistant/components/netgear/strings.json +++ b/homeassistant/components/netgear/strings.json @@ -14,7 +14,9 @@ "config": "Connection or login error: please check your configuration" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "not_ipv4_address": "No IPv4 address in ssdp discovery information", + "no_serial": "No serial number in ssdp discovery information" } }, "options": { diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index e2f1749eb76..63594587974 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.17.4"], + "requirements": ["oralb-ble==0.17.5"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@Lash-L"], "iot_class": "local_push" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 497ae364b64..d26d702430c 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -988,8 +988,10 @@ class Recorder(threading.Thread): def _handle_sqlite_corruption(self) -> None: """Handle the sqlite3 database being corrupt.""" - self._close_event_session() - self._close_connection() + try: + self._close_event_session() + finally: + self._close_connection() move_away_broken_database(dburl_to_path(self.db_url)) self.run_history.reset() self._setup_recorder() @@ -1212,18 +1214,21 @@ class Recorder(threading.Thread): """End the recorder session.""" if self.event_session is None: return - try: + if self.run_history.active: self.run_history.end(self.event_session) + try: self._commit_event_session_or_retry() - self.event_session.close() except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Error saving the event session during shutdown: %s", err) + self.event_session.close() self.run_history.clear() def _shutdown(self) -> None: """Save end time for current run.""" self.hass.add_job(self._async_stop_listeners) self._stop_executor() - self._end_session() - self._close_connection() + try: + self._end_session() + finally: + self._close_connection() diff --git a/homeassistant/components/recorder/run_history.py b/homeassistant/components/recorder/run_history.py index 02b2df066bd..63744db0b55 100644 --- a/homeassistant/components/recorder/run_history.py +++ b/homeassistant/components/recorder/run_history.py @@ -72,6 +72,11 @@ class RunHistory: start=self.recording_start, created=dt_util.utcnow() ) + @property + def active(self) -> bool: + """Return if a run is active.""" + return self._current_run_info is not None + def get(self, start: datetime) -> RecorderRuns | None: """Return the recorder run that started before or at start. @@ -141,6 +146,5 @@ class RunHistory: Must run in the recorder thread. """ - assert self._current_run_info is not None - assert self._current_run_info.end is not None - self._current_run_info = None + if self._current_run_info: + self._current_run_info = None diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index f15ce90427c..aa9e623aa6a 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -54,6 +54,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryNotReady( f"Error while trying to setup {host.api.host}:{host.api.port}: {str(err)}" ) from err + except Exception: # pylint: disable=broad-except + await host.stop() + raise config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index e44623e1a1e..983ab293f69 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -68,8 +68,6 @@ class ReolinkHost: async def async_init(self) -> None: """Connect to Reolink host.""" - self._api.expire_session() - await self._api.get_host_data() if self._api.mac_address is None: @@ -138,24 +136,27 @@ class ReolinkHost: async def disconnect(self): """Disconnect from the API, so the connection will be released.""" - await self._api.unsubscribe() - try: - await self._api.logout() - except aiohttp.ClientConnectorError as err: + await self._api.unsubscribe() + except ( + aiohttp.ClientConnectorError, + asyncio.TimeoutError, + ReolinkError, + ) as err: _LOGGER.error( - "Reolink connection error while logging out for host %s:%s: %s", + "Reolink error while unsubscribing from host %s:%s: %s", self._api.host, self._api.port, str(err), ) - except asyncio.TimeoutError: - _LOGGER.error( - "Reolink connection timeout while logging out for host %s:%s", - self._api.host, - self._api.port, - ) - except ReolinkError as err: + + try: + await self._api.logout() + except ( + aiohttp.ClientConnectorError, + asyncio.TimeoutError, + ReolinkError, + ) as err: _LOGGER.error( "Reolink error while logging out for host %s:%s: %s", self._api.host, @@ -165,13 +166,13 @@ class ReolinkHost: async def stop(self, event=None): """Disconnect the API.""" - await self.unregister_webhook() + self.unregister_webhook() await self.disconnect() async def subscribe(self) -> None: """Subscribe to motion events and register the webhook as a callback.""" if self.webhook_id is None: - await self.register_webhook() + self.register_webhook() if self._api.subscribed: _LOGGER.debug( @@ -248,7 +249,7 @@ class ReolinkHost: self._api.host, ) - async def register_webhook(self) -> None: + def register_webhook(self) -> None: """Register the webhook for motion events.""" self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}_ONVIF" event_id = self.webhook_id @@ -263,8 +264,7 @@ class ReolinkHost: try: base_url = get_url(self._hass, prefer_external=True) except NoURLAvailableError as err: - webhook.async_unregister(self._hass, event_id) - self.webhook_id = None + self.unregister_webhook() raise ReolinkWebhookException( f"Error registering URL for webhook {event_id}: " "HomeAssistant URL is not available" @@ -275,11 +275,10 @@ class ReolinkHost: _LOGGER.debug("Registered webhook: %s", event_id) - async def unregister_webhook(self): + def unregister_webhook(self): """Unregister the webhook for motion events.""" - if self.webhook_id: - _LOGGER.debug("Unregistering webhook %s", self.webhook_id) - webhook.async_unregister(self._hass, self.webhook_id) + _LOGGER.debug("Unregistering webhook %s", self.webhook_id) + webhook.async_unregister(self._hass, self.webhook_id) self.webhook_id = None async def handle_webhook( @@ -300,9 +299,10 @@ class ReolinkHost: ) return - channel = await self._api.ONVIF_event_callback(data) + channels = await self._api.ONVIF_event_callback(data) - if channel is None: + if channels is None: async_dispatcher_send(hass, f"{webhook_id}_all", {}) else: - async_dispatcher_send(hass, f"{webhook_id}_{channel}", {}) + for channel in channels: + async_dispatcher_send(hass, f"{webhook_id}_{channel}", {}) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 72ac70ef180..0519960945e 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.3.4"], + "requirements": ["reolink-aio==0.4.0"], "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index b9da3657aed..6f4724fbf00 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -2,7 +2,7 @@ "domain": "volvooncall", "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", - "requirements": ["volvooncall==0.10.1"], + "requirements": ["volvooncall==0.10.2"], "codeowners": ["@molobrakos"], "iot_class": "cloud_polling", "loggers": ["geopy", "hbmqtt", "volvooncall"], diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 372afe4b3c5..3930c50c70c 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,8 +3,7 @@ from __future__ import annotations import logging -from xiaomi_ble import SensorUpdate, XiaomiBluetoothDeviceData -from xiaomi_ble.parser import EncryptionScheme +from xiaomi_ble import EncryptionScheme, SensorUpdate, XiaomiBluetoothDeviceData from homeassistant import config_entries from homeassistant.components.bluetooth import ( diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 1de3afff53f..3d7bdfd0b48 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -1,6 +1,7 @@ """Support for Xiaomi binary sensors.""" from __future__ import annotations +from xiaomi_ble import SLEEPY_DEVICE_MODELS from xiaomi_ble.parser import ( BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, ExtendedBinarySensorDeviceClass, @@ -19,6 +20,7 @@ from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) +from homeassistant.const import ATTR_MODEL from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info @@ -128,3 +130,12 @@ class XiaomiBluetoothSensorEntity( def is_on(self) -> bool | None: """Return the native value.""" return self.processor.entity_data.get(self.entity_key) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + if self.device_info and self.device_info[ATTR_MODEL] in SLEEPY_DEVICE_MODELS: + # These devices sleep for an indeterminate amount of time + # so there is no way to track their availability. + return True + return super().available diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index f3904003411..9fb35db3248 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -14,7 +14,7 @@ } ], "dependencies": ["bluetooth_adapters"], - "requirements": ["xiaomi-ble==0.16.1"], + "requirements": ["xiaomi-ble==0.16.3"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 866f5eca6bc..df9827afcb3 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.12.8"], + "requirements": ["yalexs-ble==1.12.12"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/homeassistant/const.py b/homeassistant/const.py index 63d691f6ac7..acad89bd43f 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 = 2 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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 8dc60525be1..8ad9a144a1a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==1.0.3 bluetooth-data-tools==0.3.1 certifi>=2021.5.30 ciso8601==2.3.0 -cryptography==39.0.0 +cryptography==39.0.1 dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 diff --git a/pyproject.toml b/pyproject.toml index 6253e204117..092fdb74079 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.3" +version = "2023.2.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -41,7 +41,7 @@ dependencies = [ "lru-dict==1.1.8", "PyJWT==2.5.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==39.0.0", + "cryptography==39.0.1", # pyOpenSSL 23.0.0 is required to work with cryptography 39+ "pyOpenSSL==23.0.0", "orjson==3.8.5", diff --git a/requirements.txt b/requirements.txt index c423388bbbd..7d05e1bb2e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.5.0 -cryptography==39.0.0 +cryptography==39.0.1 pyOpenSSL==23.0.0 orjson==3.8.5 pip>=21.0,<22.4 diff --git a/requirements_all.txt b/requirements_all.txt index 2c7b26cd923..84ece70f753 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.1.0 +aioesphomeapi==13.3.1 # homeassistant.components.flo aioflo==2021.11.0 @@ -193,7 +193,7 @@ aiokafka==0.7.2 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.8.7 +aiolifx==0.8.9 # homeassistant.components.lifx aiolifx_effects==0.3.1 @@ -1299,7 +1299,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.17.4 +oralb-ble==0.17.5 # homeassistant.components.oru oru==0.1.11 @@ -1687,7 +1687,7 @@ pyinsteon==1.2.0 pyintesishome==1.8.0 # homeassistant.components.ipma -pyipma==3.0.5 +pyipma==3.0.6 # homeassistant.components.ipp pyipp==0.12.1 @@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.4 +reolink-aio==0.4.0 # homeassistant.components.python_script restrictedpython==6.0 @@ -2576,7 +2576,7 @@ vilfo-api-client==0.3.2 volkszaehler==0.4.0 # homeassistant.components.volvooncall -volvooncall==0.10.1 +volvooncall==0.10.2 # homeassistant.components.verisure vsure==1.8.1 @@ -2637,7 +2637,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.16.1 +xiaomi-ble==0.16.3 # homeassistant.components.knx xknx==2.3.0 @@ -2657,13 +2657,13 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.8 +yalexs-ble==1.12.12 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.8 +yalexs_ble==1.12.12 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91445d7ad5a..3345692e41c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.1.0 +aioesphomeapi==13.3.1 # homeassistant.components.flo aioflo==2021.11.0 @@ -174,7 +174,7 @@ aioimaplib==1.0.1 aiokafka==0.7.2 # homeassistant.components.lifx -aiolifx==0.8.7 +aiolifx==0.8.9 # homeassistant.components.lifx aiolifx_effects==0.3.1 @@ -947,7 +947,7 @@ openai==0.26.2 openerz-api==0.2.0 # homeassistant.components.oralb -oralb-ble==0.17.4 +oralb-ble==0.17.5 # homeassistant.components.ovo_energy ovoenergy==1.2.0 @@ -1209,7 +1209,7 @@ pyicloud==1.0.0 pyinsteon==1.2.0 # homeassistant.components.ipma -pyipma==3.0.5 +pyipma==3.0.6 # homeassistant.components.ipp pyipp==0.12.1 @@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.4 +reolink-aio==0.4.0 # homeassistant.components.python_script restrictedpython==6.0 @@ -1819,7 +1819,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.3.2 # homeassistant.components.volvooncall -volvooncall==0.10.1 +volvooncall==0.10.2 # homeassistant.components.verisure vsure==1.8.1 @@ -1862,7 +1862,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.16.1 +xiaomi-ble==0.16.3 # homeassistant.components.knx xknx==2.3.0 @@ -1879,13 +1879,13 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.8 +yalexs-ble==1.12.12 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.8 +yalexs_ble==1.12.12 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 7a0a7de8442..016da6fc135 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -980,7 +980,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): inject_advertisement(hass, switchbot_device, switchbot_adv_2) await hass.async_block_till_done() - assert len(mock_config_flow.mock_calls) == 2 + assert len(mock_config_flow.mock_calls) == 3 assert mock_config_flow.mock_calls[1][1][0] == "switchbot" diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index 87dbbccab08..e9d5adb0468 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -369,3 +369,28 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None: assert state.attributes.get("device_class") is None assert state.attributes.get("state_class") is None assert state.attributes.get("unit_of_measurement") is None + + +async def test_last_sensor(hass: HomeAssistant) -> None: + """Test the last sensor.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_last", + "type": "last", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_last_sensor", + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_last") + assert str(float(value)) == state.state + assert entity_id == state.attributes.get("last_entity_id") diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 4a6495d5b46..1369f09005e 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -855,7 +855,7 @@ async def test_wrap_sensor(hass, mock_do_cycle, expected): @pytest.mark.parametrize( "mock_test_state", - [(State(ENTITY_ID, "117"), State(f"{ENTITY_ID}_1", "119"))], + [(State(ENTITY_ID, "unknown"), State(f"{ENTITY_ID}_1", "119"))], indirect=True, ) @pytest.mark.parametrize( diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index 69dc57b1d2c..28ff93d2404 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -235,7 +236,26 @@ async def test_ssdp_already_configured(hass): assert result["reason"] == "already_configured" -async def test_ssdp_ipv6(hass): +async def test_ssdp_no_serial(hass: HomeAssistant) -> None: + """Test ssdp abort when the ssdp info does not include a serial number.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_URL, + upnp={ + ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", + ssdp.ATTR_UPNP_PRESENTATION_URL: URL, + }, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "no_serial" + + +async def test_ssdp_ipv6(hass: HomeAssistant) -> None: """Test ssdp abort when using a ipv6 address.""" MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/recorder/test_run_history.py b/tests/components/recorder/test_run_history.py index 3b5bd7dda6b..84dab78d8cc 100644 --- a/tests/components/recorder/test_run_history.py +++ b/tests/components/recorder/test_run_history.py @@ -1,12 +1,16 @@ """Test run history.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components import recorder from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.models import process_timestamp +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from tests.common import SetupRecorderInstanceT + async def test_run_history(recorder_mock, hass): """Test the run history gives the correct run.""" @@ -47,12 +51,32 @@ async def test_run_history(recorder_mock, hass): ) -async def test_run_history_during_schema_migration(recorder_mock, hass): - """Test the run history during schema migration.""" - instance = recorder.get_instance(hass) +async def test_run_history_while_recorder_is_not_yet_started( + async_setup_recorder_instance: SetupRecorderInstanceT, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test the run history while recorder is not yet started. + + This usually happens during schema migration because + we do not start right away. + """ + # Prevent the run history from starting to ensure + # we can test run_history.current.start returns the expected value + with patch( + "homeassistant.components.recorder.run_history.RunHistory.start", + ): + instance = await async_setup_recorder_instance(hass) run_history = instance.run_history assert run_history.current.start == run_history.recording_start - with instance.get_session() as session: - run_history.start(session) + + def _start_run_history(): + with instance.get_session() as session: + run_history.start(session) + + # Ideally we would run run_history.start in the recorder thread + # but since we mocked it out above, we run it directly here + # via the database executor to avoid blocking the event loop. + await instance.async_add_executor_job(_start_run_history) assert run_history.current.start == run_history.recording_start assert run_history.current.created >= run_history.recording_start diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 5389a2987f2..dae1569ff31 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -1,12 +1,28 @@ """Test Xiaomi binary sensors.""" +from datetime import timedelta +import time +from unittest.mock import patch + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) from homeassistant.components.xiaomi_ble.const import DOMAIN -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.util import dt as dt_util from . import make_advertisement -from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info_bleak +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info_bleak, + patch_all_discovered_devices, +) async def test_door_problem_sensors(hass): @@ -34,19 +50,19 @@ async def test_door_problem_sensors(hass): door_sensor = hass.states.get("binary_sensor.door_lock_be98_door") door_sensor_attribtes = door_sensor.attributes - assert door_sensor.state == "off" + assert door_sensor.state == STATE_OFF assert door_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door" door_left_open = hass.states.get("binary_sensor.door_lock_be98_door_left_open") door_left_open_attribtes = door_left_open.attributes - assert door_left_open.state == "off" + assert door_left_open.state == STATE_OFF assert ( door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door left open" ) pry_the_door = hass.states.get("binary_sensor.door_lock_be98_pry_the_door") pry_the_door_attribtes = pry_the_door.attributes - assert pry_the_door.state == "off" + assert pry_the_door.state == STATE_OFF assert pry_the_door_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Pry the door" assert await hass.config_entries.async_unload(entry.entry_id) @@ -77,12 +93,12 @@ async def test_light_motion(hass): motion_sensor = hass.states.get("binary_sensor.nightlight_9321_motion") motion_sensor_attribtes = motion_sensor.attributes - assert motion_sensor.state == "on" + assert motion_sensor.state == STATE_ON assert motion_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Motion" light_sensor = hass.states.get("binary_sensor.nightlight_9321_light") light_sensor_attribtes = light_sensor.attributes - assert light_sensor.state == "off" + assert light_sensor.state == STATE_OFF assert light_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Light" assert await hass.config_entries.async_unload(entry.entry_id) @@ -116,7 +132,7 @@ async def test_moisture(hass): sensor = hass.states.get("binary_sensor.smart_flower_pot_3e7a_moisture") sensor_attr = sensor.attributes - assert sensor.state == "on" + assert sensor.state == STATE_ON assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Moisture" assert await hass.config_entries.async_unload(entry.entry_id) @@ -148,12 +164,12 @@ async def test_opening(hass): opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") opening_sensor_attribtes = opening_sensor.attributes - assert opening_sensor.state == "on" + + assert opening_sensor.state == STATE_ON assert ( opening_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door/Window Sensor E567 Opening" ) - assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -183,7 +199,7 @@ async def test_opening_problem_sensors(hass): opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") opening_sensor_attribtes = opening_sensor.attributes - assert opening_sensor.state == "off" + assert opening_sensor.state == STATE_OFF assert ( opening_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door/Window Sensor E567 Opening" @@ -193,7 +209,7 @@ async def test_opening_problem_sensors(hass): "binary_sensor.door_window_sensor_e567_door_left_open" ) door_left_open_attribtes = door_left_open.attributes - assert door_left_open.state == "off" + assert door_left_open.state == STATE_OFF assert ( door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door/Window Sensor E567 Door left open" @@ -203,7 +219,7 @@ async def test_opening_problem_sensors(hass): "binary_sensor.door_window_sensor_e567_device_forcibly_removed" ) device_forcibly_removed_attribtes = device_forcibly_removed.attributes - assert device_forcibly_removed.state == "off" + assert device_forcibly_removed.state == STATE_OFF assert ( device_forcibly_removed_attribtes[ATTR_FRIENDLY_NAME] == "Door/Window Sensor E567 Device forcibly removed" @@ -238,8 +254,111 @@ async def test_smoke(hass): smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke") smoke_sensor_attribtes = smoke_sensor.attributes - assert smoke_sensor.state == "on" + assert smoke_sensor.state == STATE_ON assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_unavailable(hass): + """Test normal device goes to unavailable after 60 minutes.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + data={"bindkey": "0fdcc30fe9289254876b5ef7c11ef1f0"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"XY\x89\x18\x9ag\xe5f8\xc1\xa4\x9d\xd9z\xf3&\x00\x00\xc8\xa6\x0b\xd5", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + assert opening_sensor.state == STATE_ON + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + # Normal devices should go to unavailable + assert opening_sensor.state == STATE_UNAVAILABLE + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_sleepy_device(hass): + """Test sleepy device does not go to unavailable after 60 minutes.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"@0\xd6\x03$\x19\x10\x01\x00", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + assert opening_sensor.state == STATE_ON + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + # Sleepy devices should keep their state over time + assert opening_sensor.state == STATE_ON + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done()