This commit is contained in:
Paulus Schoutsen 2022-12-21 10:37:08 -05:00 committed by GitHub
commit 2205006d31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 259 additions and 35 deletions

View File

@ -369,6 +369,7 @@ class BluetoothManager:
all_history = self._all_history
connectable = service_info.connectable
connectable_history = self._connectable_history
old_connectable_service_info = connectable and connectable_history.get(address)
source = service_info.source
debug = _LOGGER.isEnabledFor(logging.DEBUG)
@ -399,7 +400,6 @@ class BluetoothManager:
# but not in the connectable history or the connectable source is the same
# as the new source, we need to add it to the connectable history
if connectable:
old_connectable_service_info = connectable_history.get(address)
if old_connectable_service_info and (
# If its the same as the preferred source, we are done
# as we know we prefer the old advertisement
@ -442,17 +442,24 @@ class BluetoothManager:
tracker.async_collect(service_info)
# If the advertisement data is the same as the last time we saw it, we
# don't need to do anything else.
if old_service_info and not (
service_info.manufacturer_data != old_service_info.manufacturer_data
or service_info.service_data != old_service_info.service_data
or service_info.service_uuids != old_service_info.service_uuids
or service_info.name != old_service_info.name
# don't need to do anything else unless its connectable and we are missing
# connectable history for the device so we can make it available again
# after unavailable callbacks.
if (
# Ensure its not a connectable device missing from connectable history
not (connectable and not old_connectable_service_info)
# Than check if advertisement data is the same
and old_service_info
and not (
service_info.manufacturer_data != old_service_info.manufacturer_data
or service_info.service_data != old_service_info.service_data
or service_info.service_uuids != old_service_info.service_uuids
or service_info.name != old_service_info.name
)
):
return
is_connectable_by_any_source = address in self._connectable_history
if not connectable and is_connectable_by_any_source:
if not connectable and old_connectable_service_info:
# Since we have a connectable path and our BleakClient will
# route any connection attempts to the connectable path, we
# mark the service_info as connectable so that the callbacks
@ -481,7 +488,7 @@ class BluetoothManager:
matched_domains,
)
if is_connectable_by_any_source:
if connectable or old_connectable_service_info:
# Bleak callbacks must get a connectable device
for callback_filters in self._bleak_callbacks:
_dispatch_bleak_callback(*callback_filters, device, advertisement_data)

View File

@ -10,7 +10,7 @@
"bleak-retry-connector==2.10.2",
"bluetooth-adapters==0.12.0",
"bluetooth-auto-recovery==1.0.3",
"bluetooth-data-tools==0.3.0",
"bluetooth-data-tools==0.3.1",
"dbus-fast==1.75.0"
],
"codeowners": ["@bdraco"],

View File

@ -226,14 +226,16 @@ class CloudRegisterView(HomeAssistantView):
client_metadata = None
if location_info := await async_detect_location_info(
async_get_clientsession(hass)
):
client_metadata = {
"NC_COUNTRY_CODE": location_info.country_code,
"NC_REGION_CODE": location_info.region_code,
"NC_ZIP_CODE": location_info.zip_code,
}
if (
location_info := await async_detect_location_info(
async_get_clientsession(hass)
)
) and location_info.country_code is not None:
client_metadata = {"NC_COUNTRY_CODE": location_info.country_code}
if location_info.region_code is not None:
client_metadata["NC_REGION_CODE"] = location_info.region_code
if location_info.zip_code is not None:
client_metadata["NC_ZIP_CODE"] = location_info.zip_code
async with async_timeout.timeout(REQUEST_TIMEOUT):
await cloud.auth.async_register(

View File

@ -3,7 +3,7 @@
"name": "LED BLE",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/led_ble/",
"requirements": ["bluetooth-data-tools==0.3.0", "led-ble==1.0.0"],
"requirements": ["bluetooth-data-tools==0.3.1", "led-ble==1.0.0"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"bluetooth": [

View File

@ -3,7 +3,7 @@
"name": "Local Calendar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"requirements": ["ical==4.2.3"],
"requirements": ["ical==4.2.4"],
"codeowners": ["@allenporter"],
"iot_class": "local_polling",
"loggers": ["ical"]

View File

@ -17,6 +17,8 @@ from homeassistant.const import TEMP_CELSIUS
from ..entity import OverkizEntity
PRESET_COMFORT1 = "comfort-1"
PRESET_COMFORT2 = "comfort-2"
PRESET_FROST_PROTECTION = "frost_protection"
OVERKIZ_TO_HVAC_MODES: dict[str, HVACMode] = {
@ -31,6 +33,8 @@ OVERKIZ_TO_PRESET_MODES: dict[str, str] = {
OverkizCommandParam.FROSTPROTECTION: PRESET_FROST_PROTECTION,
OverkizCommandParam.ECO: PRESET_ECO,
OverkizCommandParam.COMFORT: PRESET_COMFORT,
OverkizCommandParam.COMFORT_1: PRESET_COMFORT1,
OverkizCommandParam.COMFORT_2: PRESET_COMFORT2,
}
PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODES.items()}

View File

@ -394,7 +394,7 @@ class PrometheusMetrics:
metric.labels(**self._labels(state)).set(value)
def _handle_climate_temp(self, state, attr, metric_name, metric_description):
if temp := state.attributes.get(attr):
if (temp := state.attributes.get(attr)) is not None:
if self._climate_units == TEMP_FAHRENHEIT:
temp = TemperatureConverter.convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS)
metric = self._metric(

View File

@ -3,7 +3,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
"requirements": ["pyTibber==0.26.5"],
"requirements": ["pyTibber==0.26.6"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver",
"config_flow": true,

View File

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "7"
PATCH_VERSION: Final = "8"
__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

@ -14,7 +14,7 @@ bleak-retry-connector==2.10.2
bleak==0.19.2
bluetooth-adapters==0.12.0
bluetooth-auto-recovery==1.0.3
bluetooth-data-tools==0.3.0
bluetooth-data-tools==0.3.1
certifi>=2021.5.30
ciso8601==2.2.0
cryptography==38.0.3

View File

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

View File

@ -454,7 +454,7 @@ bluetooth-auto-recovery==1.0.3
# homeassistant.components.bluetooth
# homeassistant.components.led_ble
bluetooth-data-tools==0.3.0
bluetooth-data-tools==0.3.1
# homeassistant.components.bond
bond-async==0.1.22
@ -926,7 +926,7 @@ ibm-watson==5.2.2
ibmiotf==0.3.4
# homeassistant.components.local_calendar
ical==4.2.3
ical==4.2.4
# homeassistant.components.ping
icmplib==3.0
@ -1432,7 +1432,7 @@ pyRFXtrx==0.30.0
pySwitchmate==0.5.1
# homeassistant.components.tibber
pyTibber==0.26.5
pyTibber==0.26.6
# homeassistant.components.dlink
pyW215==0.7.0

View File

@ -368,7 +368,7 @@ bluetooth-auto-recovery==1.0.3
# homeassistant.components.bluetooth
# homeassistant.components.led_ble
bluetooth-data-tools==0.3.0
bluetooth-data-tools==0.3.1
# homeassistant.components.bond
bond-async==0.1.22
@ -691,7 +691,7 @@ iaqualink==0.5.0
ibeacon_ble==1.0.1
# homeassistant.components.local_calendar
ical==4.2.3
ical==4.2.4
# homeassistant.components.ping
icmplib==3.0
@ -1032,7 +1032,7 @@ pyMetno==0.9.0
pyRFXtrx==0.30.0
# homeassistant.components.tibber
pyTibber==0.26.5
pyTibber==0.26.6
# homeassistant.components.nextbus
py_nextbusnext==0.1.5

View File

@ -1,27 +1,46 @@
"""Tests for the Bluetooth integration manager."""
from datetime import timedelta
import time
from unittest.mock import patch
from bleak.backends.scanner import BLEDevice
from bleak.backends.scanner import AdvertisementData, BLEDevice
from bluetooth_adapters import AdvertisementHistory
import pytest
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BaseHaScanner
from homeassistant.components.bluetooth import (
BaseHaRemoteScanner,
BaseHaScanner,
BluetoothChange,
BluetoothScanningMode,
BluetoothServiceInfo,
BluetoothServiceInfoBleak,
HaBluetoothConnector,
async_ble_device_from_address,
async_get_advertisement_callback,
async_scanner_count,
async_track_unavailable,
)
from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS
from homeassistant.components.bluetooth.manager import (
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from . import (
MockBleakClient,
_get_manager,
generate_advertisement_data,
inject_advertisement_with_source,
inject_advertisement_with_time_and_source,
inject_advertisement_with_time_and_source_connectable,
)
from tests.common import async_fire_time_changed
@pytest.fixture
def register_hci0_scanner(hass: HomeAssistant) -> None:
@ -514,3 +533,172 @@ async def test_switching_adapters_when_one_stop_scanning(
)
cancel_hci2()
async def test_goes_unavailable_connectable_only_and_recovers(
hass, mock_bluetooth_adapters
):
"""Test all connectable scanners go unavailable, and than recover when there is a non-connectable scanner."""
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
await hass.async_block_till_done()
assert async_scanner_count(hass, connectable=True) == 0
assert async_scanner_count(hass, connectable=False) == 0
switchbot_device_connectable = BLEDevice(
"44:44:33:11:23:45",
"wohand",
{},
rssi=-100,
)
switchbot_device_non_connectable = BLEDevice(
"44:44:33:11:23:45",
"wohand",
{},
rssi=-100,
)
switchbot_device_adv = generate_advertisement_data(
local_name="wohand",
service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"],
service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"},
manufacturer_data={1: b"\x01"},
rssi=-100,
)
callbacks = []
def _fake_subscriber(
service_info: BluetoothServiceInfo,
change: BluetoothChange,
) -> None:
"""Fake subscriber for the BleakScanner."""
callbacks.append((service_info, change))
cancel = bluetooth.async_register_callback(
hass,
_fake_subscriber,
{"address": "44:44:33:11:23:45", "connectable": True},
BluetoothScanningMode.ACTIVE,
)
class FakeScanner(BaseHaRemoteScanner):
def inject_advertisement(
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
"""Inject an advertisement."""
self._async_on_advertisement(
device.address,
advertisement_data.rssi,
device.name,
advertisement_data.service_uuids,
advertisement_data.service_data,
advertisement_data.manufacturer_data,
advertisement_data.tx_power,
{"scanner_specific_data": "test"},
)
new_info_callback = async_get_advertisement_callback(hass)
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
connectable_scanner = FakeScanner(
hass,
"connectable",
"connectable",
new_info_callback,
connector,
True,
)
unsetup_connectable_scanner = connectable_scanner.async_setup()
cancel_connectable_scanner = _get_manager().async_register_scanner(
connectable_scanner, True
)
connectable_scanner.inject_advertisement(
switchbot_device_connectable, switchbot_device_adv
)
assert async_ble_device_from_address(hass, "44:44:33:11:23:45") is not None
assert async_scanner_count(hass, connectable=True) == 1
assert len(callbacks) == 1
assert (
"44:44:33:11:23:45"
in connectable_scanner.discovered_devices_and_advertisement_data
)
not_connectable_scanner = FakeScanner(
hass,
"not_connectable",
"not_connectable",
new_info_callback,
connector,
False,
)
unsetup_not_connectable_scanner = not_connectable_scanner.async_setup()
cancel_not_connectable_scanner = _get_manager().async_register_scanner(
not_connectable_scanner, False
)
not_connectable_scanner.inject_advertisement(
switchbot_device_non_connectable, switchbot_device_adv
)
assert async_scanner_count(hass, connectable=True) == 1
assert async_scanner_count(hass, connectable=False) == 2
assert (
"44:44:33:11:23:45"
in not_connectable_scanner.discovered_devices_and_advertisement_data
)
unavailable_callbacks: list[BluetoothServiceInfoBleak] = []
@callback
def _unavailable_callback(service_info: BluetoothServiceInfoBleak) -> None:
"""Wrong device unavailable callback."""
nonlocal unavailable_callbacks
unavailable_callbacks.append(service_info.address)
cancel_unavailable = async_track_unavailable(
hass,
_unavailable_callback,
switchbot_device_connectable.address,
connectable=True,
)
assert async_scanner_count(hass, connectable=True) == 1
cancel_connectable_scanner()
unsetup_connectable_scanner()
assert async_scanner_count(hass, connectable=True) == 0
assert async_scanner_count(hass, connectable=False) == 1
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
)
await hass.async_block_till_done()
assert "44:44:33:11:23:45" in unavailable_callbacks
cancel_unavailable()
connectable_scanner_2 = FakeScanner(
hass,
"connectable",
"connectable",
new_info_callback,
connector,
True,
)
unsetup_connectable_scanner_2 = connectable_scanner_2.async_setup()
cancel_connectable_scanner_2 = _get_manager().async_register_scanner(
connectable_scanner, True
)
connectable_scanner_2.inject_advertisement(
switchbot_device_connectable, switchbot_device_adv
)
assert (
"44:44:33:11:23:45"
in connectable_scanner_2.discovered_devices_and_advertisement_data
)
# We should get another callback to make the device available again
assert len(callbacks) == 2
cancel()
cancel_connectable_scanner_2()
unsetup_connectable_scanner_2()
cancel_not_connectable_scanner()
unsetup_not_connectable_scanner()

View File

@ -293,6 +293,12 @@ async def test_climate(client, climate_entities):
'friendly_name="Ecobee"} 24.0' in body
)
assert (
'climate_target_temperature_celsius{domain="climate",'
'entity="climate.fritzdect",'
'friendly_name="Fritz!DECT"} 0.0' in body
)
@pytest.mark.parametrize("namespace", [""])
async def test_humidifier(client, humidifier_entities):
@ -1001,6 +1007,23 @@ async def climate_fixture(hass, registry):
data["climate_2"] = climate_2
data["climate_2_attributes"] = climate_2_attributes
climate_3 = registry.async_get_or_create(
domain=climate.DOMAIN,
platform="test",
unique_id="climate_3",
unit_of_measurement=TEMP_CELSIUS,
suggested_object_id="fritzdect",
original_name="Fritz!DECT",
)
climate_3_attributes = {
ATTR_TEMPERATURE: 0,
ATTR_CURRENT_TEMPERATURE: 22,
ATTR_HVAC_ACTION: climate.HVACAction.OFF,
}
set_state_with_entry(hass, climate_3, climate.HVACAction.OFF, climate_3_attributes)
data["climate_3"] = climate_3
data["climate_3_attributes"] = climate_3_attributes
await hass.async_block_till_done()
return data