diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 534bd636355..513dc9dda14 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -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) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index ab3f9906f0b..35659a0b1db 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -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"], diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 76d8bea1664..4345afae746 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -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( diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index ea272dcacc0..9282e0bd8a2 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -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": [ diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 3e1a1afc609..21ec76bf967 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -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"] diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py index c0d1dd04663..36d657d5846 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py @@ -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()} diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index c1573755e11..954dcfd1eb1 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -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( diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 814f36e018c..cc296d06ea8 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -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, diff --git a/homeassistant/const.py b/homeassistant/const.py index 1324526804f..16015beec72 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 = 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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 674271b427f..cf6cf03c472 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 68307acc33c..8cb74b24793 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/requirements_all.txt b/requirements_all.txt index 5429021cdf5..a3bd7cfe7f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 777c4928eb1..b0fc670a396 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index e295291068a..3ec77a2ecd9 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -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() diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 1730e6c3f23..febbc72ec31 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -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