Merge pull request #76398 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-08-07 12:59:26 -04:00 committed by GitHub
commit bfb2867e8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 188 additions and 69 deletions

View File

@ -3,7 +3,7 @@
"name": "Big Ass Fans", "name": "Big Ass Fans",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/baf", "documentation": "https://www.home-assistant.io/integrations/baf",
"requirements": ["aiobafi6==0.7.0"], "requirements": ["aiobafi6==0.7.2"],
"codeowners": ["@bdraco", "@jfroy"], "codeowners": ["@bdraco", "@jfroy"],
"iot_class": "local_push", "iot_class": "local_push",
"zeroconf": [ "zeroconf": [

View File

@ -8,6 +8,7 @@ from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import Enum from enum import Enum
import logging import logging
import time
from typing import TYPE_CHECKING, Final from typing import TYPE_CHECKING, Final
import async_timeout import async_timeout
@ -56,6 +57,10 @@ START_TIMEOUT = 9
SOURCE_LOCAL: Final = "local" SOURCE_LOCAL: Final = "local"
SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5
SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT)
MONOTONIC_TIME = time.monotonic
@dataclass @dataclass
class BluetoothServiceInfoBleak(BluetoothServiceInfo): class BluetoothServiceInfoBleak(BluetoothServiceInfo):
@ -252,9 +257,10 @@ async def async_setup_entry(
) -> bool: ) -> bool:
"""Set up the bluetooth integration from a config entry.""" """Set up the bluetooth integration from a config entry."""
manager: BluetoothManager = hass.data[DOMAIN] manager: BluetoothManager = hass.data[DOMAIN]
await manager.async_start( async with manager.start_stop_lock:
BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) await manager.async_start(
) BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER)
)
entry.async_on_unload(entry.add_update_listener(_async_update_listener)) entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True return True
@ -263,8 +269,6 @@ async def _async_update_listener(
hass: HomeAssistant, entry: config_entries.ConfigEntry hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> None: ) -> None:
"""Handle options update.""" """Handle options update."""
manager: BluetoothManager = hass.data[DOMAIN]
manager.async_start_reload()
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
@ -273,7 +277,9 @@ async def async_unload_entry(
) -> bool: ) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
manager: BluetoothManager = hass.data[DOMAIN] manager: BluetoothManager = hass.data[DOMAIN]
await manager.async_stop() async with manager.start_stop_lock:
manager.async_start_reload()
await manager.async_stop()
return True return True
@ -289,13 +295,19 @@ class BluetoothManager:
self.hass = hass self.hass = hass
self._integration_matcher = integration_matcher self._integration_matcher = integration_matcher
self.scanner: HaBleakScanner | None = None self.scanner: HaBleakScanner | None = None
self.start_stop_lock = asyncio.Lock()
self._cancel_device_detected: CALLBACK_TYPE | None = None self._cancel_device_detected: CALLBACK_TYPE | None = None
self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None
self._cancel_stop: CALLBACK_TYPE | None = None
self._cancel_watchdog: CALLBACK_TYPE | None = None
self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {}
self._callbacks: list[ self._callbacks: list[
tuple[BluetoothCallback, BluetoothCallbackMatcher | None] tuple[BluetoothCallback, BluetoothCallbackMatcher | None]
] = [] ] = []
self._last_detection = 0.0
self._reloading = False self._reloading = False
self._adapter: str | None = None
self._scanning_mode = BluetoothScanningMode.ACTIVE
@hass_callback @hass_callback
def async_setup(self) -> None: def async_setup(self) -> None:
@ -317,6 +329,8 @@ class BluetoothManager:
) -> None: ) -> None:
"""Set up BT Discovery.""" """Set up BT Discovery."""
assert self.scanner is not None assert self.scanner is not None
self._adapter = adapter
self._scanning_mode = scanning_mode
if self._reloading: if self._reloading:
# On reload, we need to reset the scanner instance # On reload, we need to reset the scanner instance
# since the devices in its history may not be reachable # since the devices in its history may not be reachable
@ -381,7 +395,32 @@ class BluetoothManager:
_LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True)
raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex
self.async_setup_unavailable_tracking() self.async_setup_unavailable_tracking()
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) self._async_setup_scanner_watchdog()
self._cancel_stop = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping
)
@hass_callback
def _async_setup_scanner_watchdog(self) -> None:
"""If Dbus gets restarted or updated, we need to restart the scanner."""
self._last_detection = MONOTONIC_TIME()
self._cancel_watchdog = async_track_time_interval(
self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL
)
async def _async_scanner_watchdog(self, now: datetime) -> None:
"""Check if the scanner is running."""
time_since_last_detection = MONOTONIC_TIME() - self._last_detection
if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT:
return
_LOGGER.info(
"Bluetooth scanner has gone quiet for %s, restarting",
SCANNER_WATCHDOG_INTERVAL,
)
async with self.start_stop_lock:
self.async_start_reload()
await self.async_stop()
await self.async_start(self._scanning_mode, self._adapter)
@hass_callback @hass_callback
def async_setup_unavailable_tracking(self) -> None: def async_setup_unavailable_tracking(self) -> None:
@ -416,6 +455,7 @@ class BluetoothManager:
self, device: BLEDevice, advertisement_data: AdvertisementData self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None: ) -> None:
"""Handle a detected device.""" """Handle a detected device."""
self._last_detection = MONOTONIC_TIME()
matched_domains = self._integration_matcher.match_domains( matched_domains = self._integration_matcher.match_domains(
device, advertisement_data device, advertisement_data
) )
@ -528,14 +568,26 @@ class BluetoothManager:
for device_adv in self.scanner.history.values() for device_adv in self.scanner.history.values()
] ]
async def async_stop(self, event: Event | None = None) -> None: async def _async_hass_stopping(self, event: Event) -> None:
"""Stop the Bluetooth integration at shutdown."""
self._cancel_stop = None
await self.async_stop()
async def async_stop(self) -> None:
"""Stop bluetooth discovery.""" """Stop bluetooth discovery."""
_LOGGER.debug("Stopping bluetooth discovery")
if self._cancel_watchdog:
self._cancel_watchdog()
self._cancel_watchdog = None
if self._cancel_device_detected: if self._cancel_device_detected:
self._cancel_device_detected() self._cancel_device_detected()
self._cancel_device_detected = None self._cancel_device_detected = None
if self._cancel_unavailable_tracking: if self._cancel_unavailable_tracking:
self._cancel_unavailable_tracking() self._cancel_unavailable_tracking()
self._cancel_unavailable_tracking = None self._cancel_unavailable_tracking = None
if self._cancel_stop:
self._cancel_stop()
self._cancel_stop = None
if self.scanner: if self.scanner:
try: try:
await self.scanner.stop() # type: ignore[no-untyped-call] await self.scanner.stop() # type: ignore[no-untyped-call]

View File

@ -26,11 +26,13 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import BroadlinkDevice
from .const import DOMAIN from .const import DOMAIN
from .entity import BroadlinkEntity from .entity import BroadlinkEntity
from .helpers import data_packet, import_device, mac_address from .helpers import data_packet, import_device, mac_address
@ -80,8 +82,18 @@ async def async_setup_platform(
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
if switches := config.get(CONF_SWITCHES): if switches := config.get(CONF_SWITCHES):
platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {})
platform_data.setdefault(mac_addr, []).extend(switches) async_add_entities_config_entry: AddEntitiesCallback
device: BroadlinkDevice
async_add_entities_config_entry, device = platform_data.get(
mac_addr, (None, None)
)
if not async_add_entities_config_entry:
raise PlatformNotReady
async_add_entities_config_entry(
BroadlinkRMSwitch(device, config) for config in switches
)
else: else:
_LOGGER.warning( _LOGGER.warning(
@ -104,12 +116,8 @@ async def async_setup_entry(
switches: list[BroadlinkSwitch] = [] switches: list[BroadlinkSwitch] = []
if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}: if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}:
platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {}) platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {})
user_defined_switches = platform_data.get(device.api.mac, {}) platform_data[device.api.mac] = async_add_entities, device
switches.extend(
BroadlinkRMSwitch(device, config) for config in user_defined_switches
)
elif device.api.type == "SP1": elif device.api.type == "SP1":
switches.append(BroadlinkSP1Switch(device)) switches.append(BroadlinkSP1Switch(device))

View File

@ -3,7 +3,7 @@
"name": "deCONZ", "name": "deCONZ",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz", "documentation": "https://www.home-assistant.io/integrations/deconz",
"requirements": ["pydeconz==101"], "requirements": ["pydeconz==102"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Royal Philips Electronics", "manufacturer": "Royal Philips Electronics",

View File

@ -2,6 +2,7 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -32,7 +33,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def _async_scan_update(_=None): async def _async_scan_update(_=None):
await gree_discovery.discovery.scan() bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
await gree_discovery.discovery.scan(0, bcast_ifaces=bcast_addr)
_LOGGER.debug("Scanning network for Gree devices") _LOGGER.debug("Scanning network for Gree devices")
await _async_scan_update() await _async_scan_update()

View File

@ -1,6 +1,7 @@
"""Config flow for Gree.""" """Config flow for Gree."""
from greeclimate.discovery import Discovery from greeclimate.discovery import Discovery
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_flow from homeassistant.helpers import config_entry_flow
@ -10,7 +11,10 @@ from .const import DISCOVERY_TIMEOUT, DOMAIN
async def _async_has_devices(hass: HomeAssistant) -> bool: async def _async_has_devices(hass: HomeAssistant) -> bool:
"""Return if there are devices that can be discovered.""" """Return if there are devices that can be discovered."""
gree_discovery = Discovery(DISCOVERY_TIMEOUT) gree_discovery = Discovery(DISCOVERY_TIMEOUT)
devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT) bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
devices = await gree_discovery.scan(
wait_for=DISCOVERY_TIMEOUT, bcast_ifaces=bcast_addr
)
return len(devices) > 0 return len(devices) > 0

View File

@ -3,7 +3,8 @@
"name": "Gree Climate", "name": "Gree Climate",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gree", "documentation": "https://www.home-assistant.io/integrations/gree",
"requirements": ["greeclimate==1.2.0"], "requirements": ["greeclimate==1.3.0"],
"dependencies": ["network"],
"codeowners": ["@cmroche"], "codeowners": ["@cmroche"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["greeclimate"] "loggers": ["greeclimate"]

View File

@ -135,7 +135,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
icon="mdi:dns", icon="mdi:dns",
name="TCP Queries", name="TCP queries",
native_unit_of_measurement="queries", native_unit_of_measurement="queries",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
value=lambda data: data.tcp_queries, value=lambda data: data.tcp_queries,
@ -190,7 +190,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
icon="mdi:dns", icon="mdi:dns",
name="TCP Queries Ratio", name="TCP queries ratio",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.tcp_queries_ratio, value=lambda data: data.tcp_queries_ratio,

View File

@ -38,15 +38,22 @@ LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Philips TV from a config entry.""" """Set up Philips TV from a config entry."""
system: SystemType | None = entry.data.get(CONF_SYSTEM)
tvapi = PhilipsTV( tvapi = PhilipsTV(
entry.data[CONF_HOST], entry.data[CONF_HOST],
entry.data[CONF_API_VERSION], entry.data[CONF_API_VERSION],
username=entry.data.get(CONF_USERNAME), username=entry.data.get(CONF_USERNAME),
password=entry.data.get(CONF_PASSWORD), password=entry.data.get(CONF_PASSWORD),
system=system,
) )
coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options) coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options)
await coordinator.async_refresh() await coordinator.async_refresh()
if (actual_system := tvapi.system) and actual_system != system:
data = {**entry.data, CONF_SYSTEM: actual_system}
hass.config_entries.async_update_entry(entry, data=data)
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator

View File

@ -2,7 +2,7 @@
"domain": "switchbot", "domain": "switchbot",
"name": "SwitchBot", "name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot", "documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.17.3"], "requirements": ["PySwitchbot==0.18.4"],
"config_flow": true, "config_flow": true,
"dependencies": ["bluetooth"], "dependencies": ["bluetooth"],
"codeowners": [ "codeowners": [

View File

@ -1,7 +1,7 @@
"""Lighting channels module for Zigbee Home Automation.""" """Lighting channels module for Zigbee Home Automation."""
from __future__ import annotations from __future__ import annotations
from contextlib import suppress from functools import cached_property
from zigpy.zcl.clusters import lighting from zigpy.zcl.clusters import lighting
@ -46,17 +46,8 @@ class ColorChannel(ZigbeeChannel):
"color_loop_active": False, "color_loop_active": False,
} }
@property @cached_property
def color_capabilities(self) -> int: def color_capabilities(self) -> lighting.Color.ColorCapabilities:
"""Return color capabilities of the light."""
with suppress(KeyError):
return self.cluster["color_capabilities"]
if self.cluster.get("color_temperature") is not None:
return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP
return self.CAPABILITIES_COLOR_XY
@property
def zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities:
"""Return ZCL color capabilities of the light.""" """Return ZCL color capabilities of the light."""
color_capabilities = self.cluster.get("color_capabilities") color_capabilities = self.cluster.get("color_capabilities")
if color_capabilities is None: if color_capabilities is None:
@ -117,43 +108,41 @@ class ColorChannel(ZigbeeChannel):
def hs_supported(self) -> bool: def hs_supported(self) -> bool:
"""Return True if the channel supports hue and saturation.""" """Return True if the channel supports hue and saturation."""
return ( return (
self.zcl_color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Hue_and_saturation and lighting.Color.ColorCapabilities.Hue_and_saturation
in self.zcl_color_capabilities in self.color_capabilities
) )
@property @property
def enhanced_hue_supported(self) -> bool: def enhanced_hue_supported(self) -> bool:
"""Return True if the channel supports enhanced hue and saturation.""" """Return True if the channel supports enhanced hue and saturation."""
return ( return (
self.zcl_color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Enhanced_hue and lighting.Color.ColorCapabilities.Enhanced_hue in self.color_capabilities
in self.zcl_color_capabilities
) )
@property @property
def xy_supported(self) -> bool: def xy_supported(self) -> bool:
"""Return True if the channel supports xy.""" """Return True if the channel supports xy."""
return ( return (
self.zcl_color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.XY_attributes and lighting.Color.ColorCapabilities.XY_attributes
in self.zcl_color_capabilities in self.color_capabilities
) )
@property @property
def color_temp_supported(self) -> bool: def color_temp_supported(self) -> bool:
"""Return True if the channel supports color temperature.""" """Return True if the channel supports color temperature."""
return ( return (
self.zcl_color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_temperature and lighting.Color.ColorCapabilities.Color_temperature
in self.zcl_color_capabilities in self.color_capabilities
) ) or self.color_temperature is not None
@property @property
def color_loop_supported(self) -> bool: def color_loop_supported(self) -> bool:
"""Return True if the channel supports color loop.""" """Return True if the channel supports color loop."""
return ( return (
self.zcl_color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_loop and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities
in self.zcl_color_capabilities
) )

View File

@ -4,14 +4,14 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha", "documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [ "requirements": [
"bellows==0.31.2", "bellows==0.31.3",
"pyserial==3.5", "pyserial==3.5",
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.78", "zha-quirks==0.0.78",
"zigpy-deconz==0.18.0", "zigpy-deconz==0.18.0",
"zigpy==0.48.0", "zigpy==0.49.0",
"zigpy-xbee==0.15.0", "zigpy-xbee==0.15.0",
"zigpy-zigate==0.9.0", "zigpy-zigate==0.9.1",
"zigpy-znp==0.8.1" "zigpy-znp==0.8.1"
], ],
"usb": [ "usb": [

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 8 MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "1" PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2022.8.1" version = "2022.8.2"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"

View File

@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1 PySocks==1.7.1
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.17.3 PySwitchbot==0.18.4
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
@ -128,7 +128,7 @@ aioasuswrt==1.4.0
aioazuredevops==1.3.5 aioazuredevops==1.3.5
# homeassistant.components.baf # homeassistant.components.baf
aiobafi6==0.7.0 aiobafi6==0.7.2
# homeassistant.components.aws # homeassistant.components.aws
aiobotocore==2.1.0 aiobotocore==2.1.0
@ -396,7 +396,7 @@ beautifulsoup4==4.11.1
# beewi_smartclim==0.0.10 # beewi_smartclim==0.0.10
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.31.2 bellows==0.31.3
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.1 bimmer_connected==0.10.1
@ -769,7 +769,7 @@ gpiozero==1.6.2
gps3==0.33.3 gps3==0.33.3
# homeassistant.components.gree # homeassistant.components.gree
greeclimate==1.2.0 greeclimate==1.3.0
# homeassistant.components.greeneye_monitor # homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3 greeneye_monitor==3.0.3
@ -1455,7 +1455,7 @@ pydaikin==2.7.0
pydanfossair==0.1.0 pydanfossair==0.1.0
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==101 pydeconz==102
# homeassistant.components.delijn # homeassistant.components.delijn
pydelijn==1.0.0 pydelijn==1.0.0
@ -2529,13 +2529,13 @@ zigpy-deconz==0.18.0
zigpy-xbee==0.15.0 zigpy-xbee==0.15.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-zigate==0.9.0 zigpy-zigate==0.9.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.8.1 zigpy-znp==0.8.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.48.0 zigpy==0.49.0
# homeassistant.components.zoneminder # homeassistant.components.zoneminder
zm-py==0.5.2 zm-py==0.5.2

View File

@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1 PySocks==1.7.1
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.17.3 PySwitchbot==0.18.4
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
@ -115,7 +115,7 @@ aioasuswrt==1.4.0
aioazuredevops==1.3.5 aioazuredevops==1.3.5
# homeassistant.components.baf # homeassistant.components.baf
aiobafi6==0.7.0 aiobafi6==0.7.2
# homeassistant.components.aws # homeassistant.components.aws
aiobotocore==2.1.0 aiobotocore==2.1.0
@ -320,7 +320,7 @@ base36==0.1.1
beautifulsoup4==4.11.1 beautifulsoup4==4.11.1
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.31.2 bellows==0.31.3
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.1 bimmer_connected==0.10.1
@ -564,7 +564,7 @@ googlemaps==2.5.1
govee-ble==0.12.6 govee-ble==0.12.6
# homeassistant.components.gree # homeassistant.components.gree
greeclimate==1.2.0 greeclimate==1.3.0
# homeassistant.components.greeneye_monitor # homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3 greeneye_monitor==3.0.3
@ -1001,7 +1001,7 @@ pycoolmasternet-async==0.1.2
pydaikin==2.7.0 pydaikin==2.7.0
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==101 pydeconz==102
# homeassistant.components.dexcom # homeassistant.components.dexcom
pydexcom==0.2.3 pydexcom==0.2.3
@ -1703,13 +1703,13 @@ zigpy-deconz==0.18.0
zigpy-xbee==0.15.0 zigpy-xbee==0.15.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-zigate==0.9.0 zigpy-zigate==0.9.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.8.1 zigpy-znp==0.8.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.48.0 zigpy==0.49.0
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.39.0 zwave-js-server-python==0.39.0

View File

@ -10,6 +10,8 @@ import pytest
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import ( from homeassistant.components.bluetooth import (
SCANNER_WATCHDOG_INTERVAL,
SCANNER_WATCHDOG_TIMEOUT,
SOURCE_LOCAL, SOURCE_LOCAL,
UNAVAILABLE_TRACK_SECONDS, UNAVAILABLE_TRACK_SECONDS,
BluetoothChange, BluetoothChange,
@ -1522,3 +1524,57 @@ async def test_invalid_dbus_message(hass, caplog):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done() await hass.async_block_till_done()
assert "dbus" in caplog.text assert "dbus" in caplog.text
async def test_recovery_from_dbus_restart(
hass, mock_bleak_scanner_start, enable_bluetooth
):
"""Test we can recover when DBus gets restarted out from under us."""
assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}})
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 1
start_time_monotonic = 1000
scanner = _get_underlying_scanner()
mock_discovered = [MagicMock()]
type(scanner).discovered_devices = mock_discovered
# Ensure we don't restart the scanner if we don't need to
with patch(
"homeassistant.components.bluetooth.MONOTONIC_TIME",
return_value=start_time_monotonic + 10,
):
async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL)
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 1
# Fire a callback to reset the timer
with patch(
"homeassistant.components.bluetooth.MONOTONIC_TIME",
return_value=start_time_monotonic,
):
scanner._callback(
BLEDevice("44:44:33:11:23:42", "any_name"),
AdvertisementData(local_name="any_name"),
)
# Ensure we don't restart the scanner if we don't need to
with patch(
"homeassistant.components.bluetooth.MONOTONIC_TIME",
return_value=start_time_monotonic + 20,
):
async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL)
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 1
# We hit the timer, so we restart the scanner
with patch(
"homeassistant.components.bluetooth.MONOTONIC_TIME",
return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT,
):
async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL)
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 2

View File

@ -28,7 +28,7 @@ class FakeDiscovery:
"""Add an event listener.""" """Add an event listener."""
self._listeners.append(listener) self._listeners.append(listener)
async def scan(self, wait_for: int = 0): async def scan(self, wait_for: int = 0, bcast_ifaces=None):
"""Search for devices, return mocked data.""" """Search for devices, return mocked data."""
self.scan_count += 1 self.scan_count += 1
_LOGGER.info("CALLED SCAN %d TIMES", self.scan_count) _LOGGER.info("CALLED SCAN %d TIMES", self.scan_count)