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",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/baf",
"requirements": ["aiobafi6==0.7.0"],
"requirements": ["aiobafi6==0.7.2"],
"codeowners": ["@bdraco", "@jfroy"],
"iot_class": "local_push",
"zeroconf": [

View File

@ -8,6 +8,7 @@ from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
import logging
import time
from typing import TYPE_CHECKING, Final
import async_timeout
@ -56,6 +57,10 @@ START_TIMEOUT = 9
SOURCE_LOCAL: Final = "local"
SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5
SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT)
MONOTONIC_TIME = time.monotonic
@dataclass
class BluetoothServiceInfoBleak(BluetoothServiceInfo):
@ -252,6 +257,7 @@ async def async_setup_entry(
) -> bool:
"""Set up the bluetooth integration from a config entry."""
manager: BluetoothManager = hass.data[DOMAIN]
async with manager.start_stop_lock:
await manager.async_start(
BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER)
)
@ -263,8 +269,6 @@ async def _async_update_listener(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> None:
"""Handle options update."""
manager: BluetoothManager = hass.data[DOMAIN]
manager.async_start_reload()
await hass.config_entries.async_reload(entry.entry_id)
@ -273,6 +277,8 @@ async def async_unload_entry(
) -> bool:
"""Unload a config entry."""
manager: BluetoothManager = hass.data[DOMAIN]
async with manager.start_stop_lock:
manager.async_start_reload()
await manager.async_stop()
return True
@ -289,13 +295,19 @@ class BluetoothManager:
self.hass = hass
self._integration_matcher = integration_matcher
self.scanner: HaBleakScanner | None = None
self.start_stop_lock = asyncio.Lock()
self._cancel_device_detected: 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._callbacks: list[
tuple[BluetoothCallback, BluetoothCallbackMatcher | None]
] = []
self._last_detection = 0.0
self._reloading = False
self._adapter: str | None = None
self._scanning_mode = BluetoothScanningMode.ACTIVE
@hass_callback
def async_setup(self) -> None:
@ -317,6 +329,8 @@ class BluetoothManager:
) -> None:
"""Set up BT Discovery."""
assert self.scanner is not None
self._adapter = adapter
self._scanning_mode = scanning_mode
if self._reloading:
# On reload, we need to reset the scanner instance
# 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)
raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex
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
def async_setup_unavailable_tracking(self) -> None:
@ -416,6 +455,7 @@ class BluetoothManager:
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
"""Handle a detected device."""
self._last_detection = MONOTONIC_TIME()
matched_domains = self._integration_matcher.match_domains(
device, advertisement_data
)
@ -528,14 +568,26 @@ class BluetoothManager:
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."""
_LOGGER.debug("Stopping bluetooth discovery")
if self._cancel_watchdog:
self._cancel_watchdog()
self._cancel_watchdog = None
if self._cancel_device_detected:
self._cancel_device_detected()
self._cancel_device_detected = None
if self._cancel_unavailable_tracking:
self._cancel_unavailable_tracking()
self._cancel_unavailable_tracking = None
if self._cancel_stop:
self._cancel_stop()
self._cancel_stop = None
if self.scanner:
try:
await self.scanner.stop() # type: ignore[no-untyped-call]

View File

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

View File

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

View File

@ -2,6 +2,7 @@
from datetime import timedelta
import logging
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
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)
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")
await _async_scan_update()

View File

@ -1,6 +1,7 @@
"""Config flow for Gree."""
from greeclimate.discovery import Discovery
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.core import HomeAssistant
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:
"""Return if there are devices that can be discovered."""
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

View File

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

View File

@ -135,7 +135,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
icon="mdi:dns",
name="TCP Queries",
name="TCP queries",
native_unit_of_measurement="queries",
state_class=SensorStateClass.TOTAL,
value=lambda data: data.tcp_queries,
@ -190,7 +190,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
icon="mdi:dns",
name="TCP Queries Ratio",
name="TCP queries ratio",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
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:
"""Set up Philips TV from a config entry."""
system: SystemType | None = entry.data.get(CONF_SYSTEM)
tvapi = PhilipsTV(
entry.data[CONF_HOST],
entry.data[CONF_API_VERSION],
username=entry.data.get(CONF_USERNAME),
password=entry.data.get(CONF_PASSWORD),
system=system,
)
coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options)
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[DOMAIN][entry.entry_id] = coordinator

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,8 @@ import pytest
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
SCANNER_WATCHDOG_INTERVAL,
SCANNER_WATCHDOG_TIMEOUT,
SOURCE_LOCAL,
UNAVAILABLE_TRACK_SECONDS,
BluetoothChange,
@ -1522,3 +1524,57 @@ async def test_invalid_dbus_message(hass, caplog):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
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."""
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."""
self.scan_count += 1
_LOGGER.info("CALLED SCAN %d TIMES", self.scan_count)