From a53d1e072db1ea30fcf2bbe5b973af89e9beb218 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Nov 2022 10:56:08 +0100 Subject: [PATCH 01/50] Fix scrape scan interval (#81763) --- homeassistant/components/scrape/sensor.py | 6 ++++-- tests/components/scrape/test_sensor.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b13b5d8463b..b5ba471c301 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, + CONF_SCAN_INTERVAL, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, @@ -43,7 +44,7 @@ from .coordinator import ScrapeCoordinator _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) CONF_ATTR = "attribute" CONF_SELECT = "select" @@ -111,7 +112,8 @@ async def async_setup_platform( rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) - coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL) + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + coordinator = ScrapeCoordinator(hass, rest, scan_interval) await coordinator.async_refresh() if coordinator.data is None: raise PlatformNotReady diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index aacd89b2eb9..9affc1f9db4 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime from unittest.mock import patch -from homeassistant.components.scrape.sensor import SCAN_INTERVAL +from homeassistant.components.scrape.sensor import DEFAULT_SCAN_INTERVAL from homeassistant.components.sensor import ( CONF_STATE_CLASS, SensorDeviceClass, @@ -189,7 +189,7 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: assert state.state == "Current Version: 2021.12.10" mocker.payload = "test_scrape_sensor_no_data" - async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL) + async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") From d3bd80b87640962b0ae291987464fa3d174eab5f Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 8 Nov 2022 11:02:53 +0100 Subject: [PATCH 02/50] Fix ignored upnp discoveries not being matched when device changes its unique identifier (#81240) Fixes https://github.com/home-assistant/core/issues/78454 fixes undefined --- homeassistant/components/upnp/config_flow.py | 60 ++++++++++++++------ homeassistant/components/upnp/const.py | 1 + tests/components/upnp/test_config_flow.py | 38 +++++++++++++ 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 6b488398461..b7d6425707d 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -20,6 +20,7 @@ from .const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DOMAIN, + DOMAIN_DISCOVERIES, LOGGER, ST_IGD_V1, ST_IGD_V2, @@ -47,7 +48,7 @@ def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool: ) -async def _async_discover_igd_devices( +async def _async_discovered_igd_devices( hass: HomeAssistant, ) -> list[ssdp.SsdpServiceInfo]: """Discovery IGD devices.""" @@ -79,9 +80,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() # - user(None): scan --> user({...}) --> create_entry() - def __init__(self) -> None: - """Initialize the UPnP/IGD config flow.""" - self._discoveries: list[SsdpServiceInfo] | None = None + @property + def _discoveries(self) -> dict[str, SsdpServiceInfo]: + """Get current discoveries.""" + domain_data: dict = self.hass.data.setdefault(DOMAIN, {}) + return domain_data.setdefault(DOMAIN_DISCOVERIES, {}) + + def _add_discovery(self, discovery: SsdpServiceInfo) -> None: + """Add a discovery.""" + self._discoveries[discovery.ssdp_usn] = discovery + + def _remove_discovery(self, usn: str) -> SsdpServiceInfo: + """Remove a discovery by its USN/unique_id.""" + return self._discoveries.pop(usn) async def async_step_user( self, user_input: Mapping[str, Any] | None = None @@ -95,7 +106,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): discovery = next( iter( discovery - for discovery in self._discoveries + for discovery in self._discoveries.values() if discovery.ssdp_usn == user_input["unique_id"] ) ) @@ -103,21 +114,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_create_entry_from_discovery(discovery) # Discover devices. - discoveries = await _async_discover_igd_devices(self.hass) + discoveries = await _async_discovered_igd_devices(self.hass) # Store discoveries which have not been configured. current_unique_ids = { entry.unique_id for entry in self._async_current_entries() } - self._discoveries = [ - discovery - for discovery in discoveries + for discovery in discoveries: if ( _is_complete_discovery(discovery) and _is_igd_device(discovery) and discovery.ssdp_usn not in current_unique_ids - ) - ] + ): + self._add_discovery(discovery) # Ensure anything to add. if not self._discoveries: @@ -128,7 +137,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required("unique_id"): vol.In( { discovery.ssdp_usn: _friendly_name_from_discovery(discovery) - for discovery in self._discoveries + for discovery in self._discoveries.values() } ), } @@ -163,12 +172,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info) host = discovery_info.ssdp_headers["_host"] self._abort_if_unique_id_configured( - # Store mac address for older entries. + # Store mac address and other data for older entries. # The location is stored in the config entry such that when the location changes, the entry is reloaded. updates={ CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location, CONFIG_ENTRY_HOST: host, + CONFIG_ENTRY_ST: discovery_info.ssdp_st, }, ) @@ -204,7 +214,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="config_entry_updated") # Store discovery. - self._discoveries = [discovery_info] + self._add_discovery(discovery_info) # Ensure user recognizable. self.context["title_placeholders"] = { @@ -221,10 +231,27 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form(step_id="ssdp_confirm") - assert self._discoveries - discovery = self._discoveries[0] + assert self.unique_id + discovery = self._remove_discovery(self.unique_id) return await self._async_create_entry_from_discovery(discovery) + async def async_step_ignore(self, user_input: dict[str, Any]) -> FlowResult: + """Ignore this config flow.""" + usn = user_input["unique_id"] + discovery = self._remove_discovery(usn) + mac_address = await _async_mac_address_from_discovery(self.hass, discovery) + data = { + CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_ST: discovery.ssdp_st, + CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_MAC_ADDRESS: mac_address, + CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], + CONFIG_ENTRY_LOCATION: discovery.ssdp_location, + } + + await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) + return self.async_create_entry(title=user_input["title"], data=data) + async def _async_create_entry_from_discovery( self, discovery: SsdpServiceInfo, @@ -243,5 +270,6 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], CONFIG_ENTRY_LOCATION: discovery.ssdp_location, CONFIG_ENTRY_MAC_ADDRESS: mac_address, + CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], } return self.async_create_entry(title=title, data=data) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 8d98790983a..5f73b1e63c9 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -7,6 +7,7 @@ from homeassistant.const import TIME_SECONDS LOGGER = logging.getLogger(__package__) DOMAIN = "upnp" +DOMAIN_DISCOVERIES = "discoveries" BYTES_RECEIVED = "bytes_received" BYTES_SENT = "bytes_sent" PACKETS_RECEIVED = "packets_received" diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index f0a1de1ce37..7850554f751 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -63,6 +63,42 @@ async def test_flow_ssdp(hass: HomeAssistant): CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, + } + + +@pytest.mark.usefixtures( + "ssdp_instant_discovery", + "mock_setup_entry", + "mock_get_source_ip", + "mock_mac_address_from_host", +) +async def test_flow_ssdp_ignore(hass: HomeAssistant): + """Test config flow: discovered + ignore through ssdp.""" + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=TEST_DISCOVERY, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "ssdp_confirm" + + # Ignore entry. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": TEST_USN, "title": TEST_FRIENDLY_NAME}, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, } @@ -138,6 +174,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: None, + CONFIG_ENTRY_HOST: TEST_HOST, } @@ -382,6 +419,7 @@ async def test_flow_user(hass: HomeAssistant): CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, } From 42444872b97be4f4b749f0b6e7ae1ca9f4385da6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 15:28:47 -0500 Subject: [PATCH 03/50] Align esphome ble client notify behavior to match BlueZ (#81463) --- .../components/esphome/bluetooth/client.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 72531a2503a..c6b60831577 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -137,6 +137,7 @@ class ESPHomeClient(BaseBleakClient): was_connected = self._is_connected self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] self._is_connected = False + self._notify_cancels.clear() if self._disconnected_event: self._disconnected_event.set() self._disconnected_event = None @@ -463,12 +464,20 @@ class ESPHomeClient(BaseBleakClient): UUID or directly by the BleakGATTCharacteristic object representing it. callback (function): The function to be called on notification. """ + ble_handle = characteristic.handle + if ble_handle in self._notify_cancels: + raise BleakError( + "Notifications are already enabled on " + f"service:{characteristic.service_uuid} " + f"characteristic:{characteristic.uuid} " + f"handle:{ble_handle}" + ) cancel_coro = await self._client.bluetooth_gatt_start_notify( self._address_as_int, - characteristic.handle, + ble_handle, lambda handle, data: callback(data), ) - self._notify_cancels[characteristic.handle] = cancel_coro + self._notify_cancels[ble_handle] = cancel_coro @api_error_as_bleak_error async def stop_notify( @@ -483,5 +492,7 @@ class ESPHomeClient(BaseBleakClient): directly by the BleakGATTCharacteristic object representing it. """ characteristic = self._resolve_characteristic(char_specifier) - coro = self._notify_cancels.pop(characteristic.handle) - await coro() + # Do not raise KeyError if notifications are not enabled on this characteristic + # to be consistent with the behavior of the BlueZ backend + if coro := self._notify_cancels.pop(characteristic.handle, None): + await coro() From c93c13d8bf5ba2ea567e7cf4765aa0fef77d21c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 09:55:43 -0500 Subject: [PATCH 04/50] Bump nexia to 2.0.6 (#81474) * Bump nexia to 2.0.6 - Marks thermostat unavailable when it is offline * is property --- homeassistant/components/nexia/entity.py | 5 +++++ homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 4f806d03eda..6b017db4d34 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -80,6 +80,11 @@ class NexiaThermostatEntity(NexiaEntity): self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}" ) + @property + def available(self) -> bool: + """Return True if thermostat is available and data is available.""" + return super().available and self._thermostat.is_online + class NexiaThermostatZoneEntity(NexiaThermostatEntity): """Base class for nexia devices attached to a thermostat.""" diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 78576e06b8a..99eb7c14798 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.5"], + "requirements": ["nexia==2.0.6"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 84d01bf2361..48d7947df20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1135,7 +1135,7 @@ nettigo-air-monitor==1.4.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.5 +nexia==2.0.6 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f27a3b75f9..9007cf73452 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -825,7 +825,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.4.2 # homeassistant.components.nexia -nexia==2.0.5 +nexia==2.0.6 # homeassistant.components.discord nextcord==2.0.0a8 From 7832a7fd80de367b18c98b207c7f6dfea35c7d63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 20:35:05 +0100 Subject: [PATCH 05/50] Bump oralb-ble to 0.10.1 (#81491) fixes #81489 changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.0...v0.10.1 --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index cad6167228c..520306aed03 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.10.0"], + "requirements": ["oralb-ble==0.10.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 48d7947df20..98101012775 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.10.0 +oralb-ble==0.10.1 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9007cf73452..da1dbb63730 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.10.0 +oralb-ble==0.10.1 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 51ab5d18083e3a5a3e2493b0869778f65b041cf9 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 6 Nov 2022 08:46:00 +1100 Subject: [PATCH 06/50] Fix lifx.set_state so it works with kelvin and color_temp_kelvin and color names (#81515) --- homeassistant/components/lifx/util.py | 26 +++++- tests/components/lifx/test_light.py | 128 ++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index 135e1a7e8e9..6a9bff465ee 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -14,8 +14,11 @@ from awesomeversion import AwesomeVersion from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, + ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_XY_COLOR, ) @@ -24,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr import homeassistant.util.color as color_util -from .const import DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT +from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT FIX_MAC_FW = AwesomeVersion("3.70") @@ -80,6 +83,17 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | """ hue, saturation, brightness, kelvin = [None] * 4 + if (color_name := kwargs.get(ATTR_COLOR_NAME)) is not None: + try: + hue, saturation = color_util.color_RGB_to_hs( + *color_util.color_name_to_rgb(color_name) + ) + except ValueError: + _LOGGER.warning( + "Got unknown color %s, falling back to neutral white", color_name + ) + hue, saturation = (0, 0) + if ATTR_HS_COLOR in kwargs: hue, saturation = kwargs[ATTR_HS_COLOR] elif ATTR_RGB_COLOR in kwargs: @@ -93,6 +107,13 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | saturation = int(saturation / 100 * 65535) kelvin = 3500 + if ATTR_KELVIN in kwargs: + _LOGGER.warning( + "The 'kelvin' parameter is deprecated. Please use 'color_temp_kelvin' for all service calls" + ) + kelvin = kwargs.pop(ATTR_KELVIN) + saturation = 0 + if ATTR_COLOR_TEMP_KELVIN in kwargs: kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN) saturation = 0 @@ -100,6 +121,9 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | if ATTR_BRIGHTNESS in kwargs: brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + if ATTR_BRIGHTNESS_PCT in kwargs: + brightness = convert_8_to_16(round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100)) + hsbk = [hue, saturation, brightness, kelvin] return None if hsbk == [None] * 4 else hsbk diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 6fe63b14b6a..5a9b250034a 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -21,11 +21,14 @@ from homeassistant.components.lifx.manager import ( ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, ATTR_COLOR_MODE, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, @@ -1397,6 +1400,131 @@ async def test_transitions_color_bulb(hass: HomeAssistant) -> None: bulb.set_color.reset_mock() +async def test_lifx_set_state_color(hass: HomeAssistant) -> None: + """Test lifx.set_state works with color names and RGB.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 2700] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + # brightness should convert from 8 to 16 bits + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 65535, 2700] + bulb.set_color.reset_mock() + + # brightness_pct should convert into 16 bit + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 90}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 59110, 2700] + bulb.set_color.reset_mock() + + # color name should turn into hue, saturation + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "red", ATTR_BRIGHTNESS_PCT: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [0, 65535, 65535, 3500] + bulb.set_color.reset_mock() + + # unknown color name should reset back to neutral white, i.e. 3500K + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "deepblack"}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [0, 0, 32000, 3500] + bulb.set_color.reset_mock() + + # RGB should convert to hue, saturation + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (0, 255, 0)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [21845, 65535, 32000, 3500] + bulb.set_color.reset_mock() + + # XY should convert to hue, saturation + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.34, 0.339)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [5461, 5139, 32000, 3500] + bulb.set_color.reset_mock() + + +async def test_lifx_set_state_kelvin(hass: HomeAssistant) -> None: + """Test set_state works with old and new kelvin parameter names.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255, ATTR_KELVIN: 3500}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP_KELVIN: 2700}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 25700, 2700] + bulb.set_color.reset_mock() + + async def test_infrared_color_bulb(hass: HomeAssistant) -> None: """Test setting infrared with a color bulb.""" already_migrated_config_entry = MockConfigEntry( From 7b769b39c2ffe4255e034c1bbc66eae8761bc937 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 09:57:32 -0500 Subject: [PATCH 07/50] Add additional coverage for adding multiple elkm1 instances (#81528) * Add additional coverage for adding multiple elkm1 instances * fix copy error --- tests/components/elkm1/test_config_flow.py | 137 +++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index e47dc402b64..7ce0e2163ac 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -2,6 +2,7 @@ from dataclasses import asdict from unittest.mock import patch +from elkm1_lib.discovery import ElkSystem import pytest from homeassistant import config_entries @@ -1317,3 +1318,139 @@ async def test_discovered_by_dhcp_no_udp_response(hass): assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" + + +async def test_multiple_instances_with_discovery(hass): + """Test we can setup a secure elk.""" + + elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601) + elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601) + + with _patch_discovery(device=elk_discovery_1): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_1.mac_address}, + ) + await hass.async_block_till_done() + + with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeeff" + assert result3["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.1", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # Now try to add another instance with the different discovery info + with _patch_discovery(device=elk_discovery_2): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_2.mac_address}, + ) + await hass.async_block_till_done() + + with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeefe" + assert result3["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.2", + "password": "test-password", + "prefix": "ddeefe", + "username": "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 + + # Finally, try to add another instance manually with no discovery info + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "prefix": "guest_house", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "guest_house" + assert result2["data"] == { + "auto_configure": True, + "host": "elk://1.2.3.4", + "prefix": "guest_house", + "username": "", + "password": "", + } + assert len(mock_setup_entry.mock_calls) == 1 From 087ede959dc50509297462edc0f9f3b88f364267 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Nov 2022 14:42:37 +0100 Subject: [PATCH 08/50] Bump oralb-ble to 0.10.2 (#81537) Fixes some more missing pressure mappings changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.1...v0.10.2 --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 520306aed03..ba89c73a240 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.10.1"], + "requirements": ["oralb-ble==0.10.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 98101012775..88208e1e0f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.10.1 +oralb-ble==0.10.2 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da1dbb63730..7fdd465045f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.10.1 +oralb-ble==0.10.2 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 7124cedd7a02e7d11977d2970a84df2f7707d1a1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 5 Nov 2022 11:58:47 -0600 Subject: [PATCH 09/50] Bump pyairvisual to 2022.11.1 (#81556) --- homeassistant/components/airvisual/__init__.py | 10 +++------- homeassistant/components/airvisual/config_flow.py | 6 +++--- homeassistant/components/airvisual/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airvisual/test_config_flow.py | 6 +++--- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index a2a3d76c3db..2a544edb20a 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -7,13 +7,9 @@ from math import ceil from typing import Any from pyairvisual import CloudAPI, NodeSamba -from pyairvisual.errors import ( - AirVisualError, - InvalidKeyError, - KeyExpiredError, - NodeProError, - UnauthorizedError, -) +from pyairvisual.cloud_api import InvalidKeyError, KeyExpiredError, UnauthorizedError +from pyairvisual.errors import AirVisualError +from pyairvisual.node import NodeProError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 385c9f55753..9510c938cb0 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -6,14 +6,14 @@ from collections.abc import Mapping from typing import Any from pyairvisual import CloudAPI, NodeSamba -from pyairvisual.errors import ( - AirVisualError, +from pyairvisual.cloud_api import ( InvalidKeyError, KeyExpiredError, - NodeProError, NotFoundError, UnauthorizedError, ) +from pyairvisual.errors import AirVisualError +from pyairvisual.node import NodeProError import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index 73bbf0cd589..ae9eeb270a8 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,7 +3,7 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==2022.07.0"], + "requirements": ["pyairvisual==2022.11.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyairvisual", "pysmb"], diff --git a/requirements_all.txt b/requirements_all.txt index 88208e1e0f4..397093afbbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1433,7 +1433,7 @@ pyaftership==21.11.0 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==2022.07.0 +pyairvisual==2022.11.1 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7fdd465045f..50147ff604f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,7 +1021,7 @@ pyaehw4a1==0.3.9 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==2022.07.0 +pyairvisual==2022.11.1 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index f97ee845dba..7603917eddc 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,14 +1,14 @@ """Define tests for the AirVisual config flow.""" from unittest.mock import patch -from pyairvisual.errors import ( - AirVisualError, +from pyairvisual.cloud_api import ( InvalidKeyError, KeyExpiredError, - NodeProError, NotFoundError, UnauthorizedError, ) +from pyairvisual.errors import AirVisualError +from pyairvisual.node import NodeProError import pytest from homeassistant import data_entry_flow From 2a34d3a56ffb14d495387c2cef86bbddabc7e381 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 5 Nov 2022 17:06:34 -0700 Subject: [PATCH 10/50] Bump gcal_sync to 4.0.0 (#81562) * Bump gcal_sync to 2.2.4 * Bump gcal sync to 4.0.0 * Add Calendar accessRole fields which are now required --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google/test_config_flow.py | 2 +- tests/components/google/test_init.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index f6ebc665cd7..9fc265fa287 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==2.2.3", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.0.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 397093afbbb..5e133c0058f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -725,7 +725,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.3 +gcal-sync==4.0.0 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50147ff604f..8c1ae96837f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -541,7 +541,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.3 +gcal-sync==4.0.0 # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index d8ddd6fe588..bce3f4855c7 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -104,7 +104,7 @@ async def primary_calendar( """Fixture to return the primary calendar.""" mock_calendar_get( "primary", - {"id": primary_calendar_email, "summary": "Personal"}, + {"id": primary_calendar_email, "summary": "Personal", "accessRole": "owner"}, exc=primary_calendar_error, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 5e7696eec68..a2f16f778fd 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -768,7 +768,7 @@ async def test_assign_unique_id( mock_calendar_get( "primary", - {"id": EMAIL_ADDRESS, "summary": "Personal"}, + {"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "reader"}, ) mock_calendars_list({"items": [test_api_calendar]}) From bf5ecc30ed706735fd5ef2480529759a294f2c0d Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 4 Nov 2022 18:29:00 +0200 Subject: [PATCH 11/50] Fix Shelly Plus HT missing battery entity (#81564) --- homeassistant/components/shelly/sensor.py | 8 +------- homeassistant/components/shelly/utils.py | 7 ------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 3ddabf7ca2b..cf1eb7508c7 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -47,12 +47,7 @@ from .entity import ( async_setup_entry_rest, async_setup_entry_rpc, ) -from .utils import ( - get_device_entry_gen, - get_device_uptime, - is_rpc_device_externally_powered, - temperature_unit, -) +from .utils import get_device_entry_gen, get_device_uptime, temperature_unit @dataclass @@ -407,7 +402,6 @@ RPC_SENSORS: Final = { value=lambda status, _: status["percent"], device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, - removal_condition=is_rpc_device_externally_powered, entity_registry_enabled_default=True, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index c3b6d24752f..a13d84d32be 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -364,13 +364,6 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool: return con_types is not None and con_types[channel].lower().startswith("light") -def is_rpc_device_externally_powered( - config: dict[str, Any], status: dict[str, Any], key: str -) -> bool: - """Return true if device has external power instead of battery.""" - return cast(bool, status[key]["external"]["present"]) - - def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]: """Return list of input triggers for RPC device.""" triggers = [] From d24e272d5e5099b15f7c3f99d7ccf5382a352b98 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 6 Nov 2022 14:51:19 +0100 Subject: [PATCH 12/50] Fix watermeter issue for old P1 Monitor versions (#81570) * Bump the python package version * Add exception to check if user has a water meter --- homeassistant/components/p1_monitor/__init__.py | 5 +++-- homeassistant/components/p1_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index b157f3e8116..e6178ffeb41 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -5,6 +5,7 @@ from typing import TypedDict from p1monitor import ( P1Monitor, + P1MonitorConnectionError, P1MonitorNoDataError, Phases, Settings, @@ -101,8 +102,8 @@ class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]): try: data[SERVICE_WATERMETER] = await self.p1monitor.watermeter() self.has_water_meter = True - except P1MonitorNoDataError: - LOGGER.debug("No watermeter data received from P1 Monitor") + except (P1MonitorNoDataError, P1MonitorConnectionError): + LOGGER.debug("No water meter data received from P1 Monitor") if self.has_water_meter is None: self.has_water_meter = False diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 626cff15dfa..2e699276caa 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -3,7 +3,7 @@ "name": "P1 Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/p1_monitor", - "requirements": ["p1monitor==2.1.0"], + "requirements": ["p1monitor==2.1.1"], "codeowners": ["@klaasnicolaas"], "quality_scale": "platinum", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 5e133c0058f..35a09916fc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1250,7 +1250,7 @@ orvibo==1.1.1 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==2.1.0 +p1monitor==2.1.1 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c1ae96837f..7d7325c4467 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -889,7 +889,7 @@ oralb-ble==0.10.2 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==2.1.0 +p1monitor==2.1.1 # homeassistant.components.mqtt # homeassistant.components.shiftr From a8e1afb9660a72a46951dd54799a17ca0fbe59ab Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 5 Nov 2022 08:57:57 -0600 Subject: [PATCH 13/50] Bump pylitterbot to 2022.11.0 (#81572) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 0965670e569..6384df2f25a 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.10.2"], + "requirements": ["pylitterbot==2022.11.0"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 35a09916fc9..fa541d0d0fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1688,7 +1688,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.10.2 +pylitterbot==2022.11.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d7325c4467..42bae213644 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1189,7 +1189,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.10.2 +pylitterbot==2022.11.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 From 6110700e1812857eb3decc9b70dafb905f87654b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:03:37 -0600 Subject: [PATCH 14/50] Fix HomeKit reset accessory procedure (#81573) fixes https://github.com/home-assistant/core/issues/81571 --- homeassistant/components/homekit/__init__.py | 28 ++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index b809f6db205..333d6052d17 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -10,8 +10,10 @@ import os from typing import Any, cast from aiohttp import web +from pyhap.characteristic import Characteristic from pyhap.const import STANDALONE_AID from pyhap.loader import get_loader +from pyhap.service import Service import voluptuous as vol from zeroconf.asyncio import AsyncZeroconf @@ -139,7 +141,7 @@ STATUS_WAIT = 3 PORT_CLEANUP_CHECK_INTERVAL_SECS = 1 _HOMEKIT_CONFIG_UPDATE_TIME = ( - 5 # number of seconds to wait for homekit to see the c# change + 10 # number of seconds to wait for homekit to see the c# change ) @@ -529,6 +531,7 @@ class HomeKit: self.status = STATUS_READY self.driver: HomeDriver | None = None self.bridge: HomeBridge | None = None + self._reset_lock = asyncio.Lock() def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: """Set up bridge and accessory driver.""" @@ -558,21 +561,24 @@ class HomeKit: async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None: """Reset the accessory to load the latest configuration.""" - if not self.bridge: - await self.async_reset_accessories_in_accessory_mode(entity_ids) - return - await self.async_reset_accessories_in_bridge_mode(entity_ids) + async with self._reset_lock: + if not self.bridge: + await self.async_reset_accessories_in_accessory_mode(entity_ids) + return + await self.async_reset_accessories_in_bridge_mode(entity_ids) async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None: """Shutdown an accessory.""" assert self.driver is not None await accessory.stop() # Deallocate the IIDs for the accessory - iid_manager = self.driver.iid_manager - for service in accessory.services: - iid_manager.remove_iid(iid_manager.remove_obj(service)) - for char in service.characteristics: - iid_manager.remove_iid(iid_manager.remove_obj(char)) + iid_manager = accessory.iid_manager + services: list[Service] = accessory.services + for service in services: + iid_manager.remove_obj(service) + characteristics: list[Characteristic] = service.characteristics + for char in characteristics: + iid_manager.remove_obj(char) async def async_reset_accessories_in_accessory_mode( self, entity_ids: Iterable[str] @@ -581,7 +587,6 @@ class HomeKit: assert self.driver is not None acc = cast(HomeAccessory, self.driver.accessory) - await self._async_shutdown_accessory(acc) if acc.entity_id not in entity_ids: return if not (state := self.hass.states.get(acc.entity_id)): @@ -589,6 +594,7 @@ class HomeKit: "The underlying entity %s disappeared during reset", acc.entity_id ) return + await self._async_shutdown_accessory(acc) if new_acc := self._async_create_single_accessory([state]): self.driver.accessory = new_acc self.hass.async_add_job(new_acc.run) From 55c87c733a65c6014ead909864cc738b9de500bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:04:24 -0600 Subject: [PATCH 15/50] Ensure HomeKit temperature controls appear before fan controls on thermostat accessories (#81586) --- homeassistant/components/homekit/type_thermostats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index a8c7a53718a..a924548816b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -306,7 +306,7 @@ class Thermostat(HomeAccessory): if attributes.get(ATTR_HVAC_ACTION) is not None: self.fan_chars.append(CHAR_CURRENT_FAN_STATE) serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars) - serv_fan.add_linked_service(serv_thermostat) + serv_thermostat.add_linked_service(serv_fan) self.char_active = serv_fan.configure_char( CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active ) From f9c77320903880f814cad9690fe0222509dc7b49 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 7 Nov 2022 06:28:59 -0500 Subject: [PATCH 16/50] Bump ZHA quirks and associated changes (#81587) --- .../zha/core/channels/manufacturerspecific.py | 74 ++++++++----------- homeassistant/components/zha/device_action.py | 31 +++++--- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/switch.py | 12 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_device_action.py | 2 +- 7 files changed, 67 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 814e7700d01..c4baccf4ae6 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, Any -from zigpy import types +from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType from zigpy.exceptions import ZigbeeException import zigpy.zcl @@ -183,59 +183,47 @@ class InovelliNotificationChannel(ClientChannel): class InovelliConfigEntityChannel(ZigbeeChannel): """Inovelli Configuration Entity channel.""" - class LEDEffectType(types.enum8): - """Effect type for Inovelli Blue Series switch.""" - - Off = 0x00 - Solid = 0x01 - Fast_Blink = 0x02 - Slow_Blink = 0x03 - Pulse = 0x04 - Chase = 0x05 - Open_Close = 0x06 - Small_To_Big = 0x07 - Clear = 0xFF - REPORT_CONFIG = () ZCL_INIT_ATTRS = { - "dimming_speed_up_remote": False, - "dimming_speed_up_local": False, - "ramp_rate_off_to_on_local": False, - "ramp_rate_off_to_on_remote": False, - "dimming_speed_down_remote": False, - "dimming_speed_down_local": False, - "ramp_rate_on_to_off_local": False, - "ramp_rate_on_to_off_remote": False, - "minimum_level": False, - "maximum_level": False, - "invert_switch": False, - "auto_off_timer": False, - "default_level_local": False, - "default_level_remote": False, - "state_after_power_restored": False, - "load_level_indicator_timeout": False, - "active_power_reports": False, - "periodic_power_and_energy_reports": False, - "active_energy_reports": False, + "dimming_speed_up_remote": True, + "dimming_speed_up_local": True, + "ramp_rate_off_to_on_local": True, + "ramp_rate_off_to_on_remote": True, + "dimming_speed_down_remote": True, + "dimming_speed_down_local": True, + "ramp_rate_on_to_off_local": True, + "ramp_rate_on_to_off_remote": True, + "minimum_level": True, + "maximum_level": True, + "invert_switch": True, + "auto_off_timer": True, + "default_level_local": True, + "default_level_remote": True, + "state_after_power_restored": True, + "load_level_indicator_timeout": True, + "active_power_reports": True, + "periodic_power_and_energy_reports": True, + "active_energy_reports": True, "power_type": False, "switch_type": False, "button_delay": False, "smart_bulb_mode": False, - "double_tap_up_for_full_brightness": False, - "led_color_when_on": False, - "led_color_when_off": False, - "led_intensity_when_on": False, - "led_intensity_when_off": False, + "double_tap_up_for_full_brightness": True, + "led_color_when_on": True, + "led_color_when_off": True, + "led_intensity_when_on": True, + "led_intensity_when_off": True, "local_protection": False, "output_mode": False, - "on_off_led_mode": False, - "firmware_progress_led": False, - "relay_click_in_on_off_mode": False, + "on_off_led_mode": True, + "firmware_progress_led": True, + "relay_click_in_on_off_mode": True, + "disable_clear_notifications_double_tap": True, } async def issue_all_led_effect( self, - effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink, + effect_type: AllLEDEffectType | int = AllLEDEffectType.Fast_Blink, color: int = 200, level: int = 100, duration: int = 3, @@ -251,7 +239,7 @@ class InovelliConfigEntityChannel(ZigbeeChannel): async def issue_individual_led_effect( self, led_number: int = 1, - effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink, + effect_type: SingleLEDEffectType | int = SingleLEDEffectType.Fast_Blink, color: int = 200, level: int = 100, duration: int = 3, diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 3e2a3591c80..01a08bc2f32 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN -from .core.channels.manufacturerspecific import InovelliConfigEntityChannel +from .core.channels.manufacturerspecific import AllLEDEffectType, SingleLEDEffectType from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI from .core.helpers import async_get_zha_device @@ -40,9 +40,7 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): INOVELLI_ALL_LED_EFFECT, vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required( - "effect_type" - ): InovelliConfigEntityChannel.LEDEffectType.__getitem__, + vol.Required("effect_type"): AllLEDEffectType.__getitem__, vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), @@ -52,10 +50,16 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA = INOVELLI_ALL_LED_EFFECT_SCHEMA.extend( { vol.Required(CONF_TYPE): INOVELLI_INDIVIDUAL_LED_EFFECT, - vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)), + vol.Required("effect_type"): SingleLEDEffectType.__getitem__, + vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), } ) +ACTION_SCHEMA_MAP = { + INOVELLI_ALL_LED_EFFECT: INOVELLI_ALL_LED_EFFECT_SCHEMA, + INOVELLI_INDIVIDUAL_LED_EFFECT: INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA, +} + ACTION_SCHEMA = vol.Any( INOVELLI_ALL_LED_EFFECT_SCHEMA, INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA, @@ -83,9 +87,7 @@ DEVICE_ACTION_TYPES = { DEVICE_ACTION_SCHEMAS = { INOVELLI_ALL_LED_EFFECT: vol.Schema( { - vol.Required("effect_type"): vol.In( - InovelliConfigEntityChannel.LEDEffectType.__members__.keys() - ), + vol.Required("effect_type"): vol.In(AllLEDEffectType.__members__.keys()), vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), @@ -94,9 +96,7 @@ DEVICE_ACTION_SCHEMAS = { INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema( { vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), - vol.Required("effect_type"): vol.In( - InovelliConfigEntityChannel.LEDEffectType.__members__.keys() - ), + vol.Required("effect_type"): vol.In(SingleLEDEffectType.__members__.keys()), vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), @@ -127,6 +127,15 @@ async def async_call_action_from_config( ) +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + schema = ACTION_SCHEMA_MAP.get(config[CONF_TYPE], DEFAULT_ACTION_SCHEMA) + config = schema(config) + return config + + async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e40a54c11bc..c8aebe3b0c0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.2", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.84", + "zha-quirks==0.0.85", "zigpy-deconz==0.19.0", "zigpy==0.51.5", "zigpy-xbee==0.16.2", diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 0bd55cdbe68..0c2e5e7ebe2 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -418,3 +418,15 @@ class InovelliRelayClickInOnOffMode( _zcl_attribute: str = "relay_click_in_on_off_mode" _attr_name: str = "Disable relay click in on off mode" + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_INOVELLI, +) +class InovelliDisableDoubleTapClearNotificationsMode( + ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap" +): + """Inovelli disable clear notifications double tap control.""" + + _zcl_attribute: str = "disable_clear_notifications_double_tap" + _attr_name: str = "Disable config 2x tap to clear notifications" diff --git a/requirements_all.txt b/requirements_all.txt index fa541d0d0fa..0880ff772b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2607,7 +2607,7 @@ zengge==0.2 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.84 +zha-quirks==0.0.85 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42bae213644..20f356af540 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1808,7 +1808,7 @@ zamg==0.1.1 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.84 +zha-quirks==0.0.85 # homeassistant.components.zha zigpy-deconz==0.19.0 diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 584abbaecdb..19125558b52 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -290,7 +290,7 @@ async def test_action(hass, device_ias, device_inovelli): "domain": DOMAIN, "device_id": inovelli_reg_device.id, "type": "issue_individual_led_effect", - "effect_type": "Open_Close", + "effect_type": "Falling", "led_number": 1, "duration": 5, "level": 10, From 5a6423a944fa43315efb3cded93dcb837dab63ed Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 6 Nov 2022 21:23:48 +0100 Subject: [PATCH 17/50] Always use Celsius in Shelly integration, part 2 (#81602) * Always use Celsius in Shelly integration * Update homeassistant/components/shelly/sensor.py Co-authored-by: Aarni Koskela * Restore unit from the registry during HA startup Co-authored-by: Aarni Koskela --- homeassistant/components/shelly/entity.py | 5 ++++ homeassistant/components/shelly/sensor.py | 30 +++++++++++++++-------- homeassistant/components/shelly/utils.py | 11 ++------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index fd92ea41408..96f566f6a2e 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -9,6 +9,7 @@ from aioshelly.block_device import Block from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry, entity, entity_registry @@ -615,6 +616,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti """Initialize the sleeping sensor.""" self.sensors = sensors self.last_state: StateType = None + self.last_unit: str | None = None self.coordinator = coordinator self.attribute = attribute self.block: Block | None = block # type: ignore[assignment] @@ -644,6 +646,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti if last_state is not None: self.last_state = last_state.state + self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @callback def _update_callback(self) -> None: @@ -696,6 +699,7 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity): ) -> None: """Initialize the sleeping sensor.""" self.last_state: StateType = None + self.last_unit: str | None = None self.coordinator = coordinator self.key = key self.attribute = attribute @@ -725,3 +729,4 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity): if last_state is not None: self.last_state = last_state.state + self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index cf1eb7508c7..922a5ac8b5c 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -47,7 +47,7 @@ from .entity import ( async_setup_entry_rest, async_setup_entry_rpc, ) -from .utils import get_device_entry_gen, get_device_uptime, temperature_unit +from .utils import get_device_entry_gen, get_device_uptime @dataclass @@ -79,7 +79,7 @@ SENSORS: Final = { ("device", "deviceTemp"): BlockSensorDescription( key="device|deviceTemp", name="Device Temperature", - unit_fn=temperature_unit, + native_unit_of_measurement=TEMP_CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -221,7 +221,7 @@ SENSORS: Final = { ("sensor", "temp"): BlockSensorDescription( key="sensor|temp", name="Temperature", - unit_fn=temperature_unit, + native_unit_of_measurement=TEMP_CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -230,7 +230,7 @@ SENSORS: Final = { ("sensor", "extTemp"): BlockSensorDescription( key="sensor|extTemp", name="Temperature", - unit_fn=temperature_unit, + native_unit_of_measurement=TEMP_CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -499,8 +499,6 @@ class BlockSensor(ShellyBlockAttributeEntity, SensorEntity): super().__init__(coordinator, block, attribute, description) self._attr_native_unit_of_measurement = description.native_unit_of_measurement - if unit_fn := description.unit_fn: - self._attr_native_unit_of_measurement = unit_fn(block.info(attribute)) @property def native_value(self) -> StateType: @@ -547,10 +545,6 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): """Initialize the sleeping sensor.""" super().__init__(coordinator, block, attribute, description, entry, sensors) - self._attr_native_unit_of_measurement = description.native_unit_of_measurement - if block and (unit_fn := description.unit_fn): - self._attr_native_unit_of_measurement = unit_fn(block.info(attribute)) - @property def native_value(self) -> StateType: """Return value of sensor.""" @@ -559,6 +553,14 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): return self.last_state + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + if self.block is not None: + return self.entity_description.native_unit_of_measurement + + return self.last_unit + class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity): """Represent a RPC sleeping sensor.""" @@ -572,3 +574,11 @@ class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity): return self.attribute_value return self.last_state + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + if self.coordinator.device.initialized: + return self.entity_description.native_unit_of_measurement + + return self.last_unit diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index a13d84d32be..bf242d47e6c 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -5,13 +5,13 @@ from datetime import datetime, timedelta from typing import Any, cast from aiohttp.web import Request, WebSocketResponse -from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice +from aioshelly.block_device import COAP, Block, BlockDevice from aioshelly.const import MODEL_NAMES from aioshelly.rpc_device import RpcDevice, WsServer from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry, entity_registry, singleton from homeassistant.helpers.typing import EventType @@ -43,13 +43,6 @@ def async_remove_shelly_entity( entity_reg.async_remove(entity_id) -def temperature_unit(block_info: dict[str, Any]) -> str: - """Detect temperature unit.""" - if block_info[BLOCK_VALUE_UNIT] == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - def get_block_device_name(device: BlockDevice) -> str: """Naming for device.""" return cast(str, device.settings["name"] or device.settings["device"]["hostname"]) From 0983f8aadf8ab512a06fc5961f941360379b5704 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 6 Nov 2022 13:02:59 +0100 Subject: [PATCH 18/50] Bump PyXiaomiGateway to 0.14.3 (#81603) Fixes: #80249 --- homeassistant/components/xiaomi_aqara/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index a70fb90f961..8152a77a73e 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Gateway (Aqara)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", - "requirements": ["PyXiaomiGateway==0.14.1"], + "requirements": ["PyXiaomiGateway==0.14.3"], "after_dependencies": ["discovery"], "codeowners": ["@danielhiversen", "@syssi"], "zeroconf": ["_miio._udp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 0880ff772b0..ecbe0b8e6b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,7 +50,7 @@ PyTurboJPEG==1.6.7 PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.14.1 +PyXiaomiGateway==0.14.3 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20f356af540..c594deaa475 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -46,7 +46,7 @@ PyTurboJPEG==1.6.7 PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.14.1 +PyXiaomiGateway==0.14.3 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 From 9771147a1e05c87fc1eb22422e93cd2711196914 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 5 Nov 2022 08:40:28 -0400 Subject: [PATCH 19/50] Fix invalid min and max color temp in bad ZHA light devices (#81604) * Fix ZHA default color temps * update test --- .../components/zha/core/channels/lighting.py | 18 ++++++++++++++++-- tests/components/zha/test_light.py | 6 +++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index e70eea11a87..ffbbc32a7a5 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -98,12 +98,26 @@ class ColorChannel(ZigbeeChannel): @property def min_mireds(self) -> int: """Return the coldest color_temp that this channel supports.""" - return self.cluster.get("color_temp_physical_min", self.MIN_MIREDS) + min_mireds = self.cluster.get("color_temp_physical_min", self.MIN_MIREDS) + if min_mireds == 0: + self.warning( + "[Min mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected", + self.MIN_MIREDS, + ) + min_mireds = self.MIN_MIREDS + return min_mireds @property def max_mireds(self) -> int: """Return the warmest color_temp that this channel supports.""" - return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) + max_mireds = self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) + if max_mireds == 0: + self.warning( + "[Max mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected", + self.MAX_MIREDS, + ) + max_mireds = self.MAX_MIREDS + return max_mireds @property def hs_supported(self) -> bool: diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index a9b8c7a14ee..d40605f81dc 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -242,7 +242,9 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature - | lighting.Color.ColorCapabilities.XY_attributes + | lighting.Color.ColorCapabilities.XY_attributes, + "color_temp_physical_min": 0, + "color_temp_physical_max": 0, } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -1192,6 +1194,8 @@ async def test_transitions( assert eWeLink_state.state == STATE_ON assert eWeLink_state.attributes["color_temp"] == 235 assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + assert eWeLink_state.attributes["min_mireds"] == 153 + assert eWeLink_state.attributes["max_mireds"] == 500 async def async_test_on_off_from_light(hass, cluster, entity_id): From 9beb9f6fc027b165f21d2fa9526d0cce72f8b8c2 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 5 Nov 2022 23:11:59 +0100 Subject: [PATCH 20/50] Fix repeating SSDP errors by checking address scope_ids and proper hostname (#81611) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 9d05b02000b..e4620386b98 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.32.1"], + "requirements": ["async-upnp-client==0.32.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 98ad81e653a..7c7a312159b 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.32.1"], + "requirements": ["async-upnp-client==0.32.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index a7aa842f485..b7988a558bd 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.1.0", - "async-upnp-client==0.32.1" + "async-upnp-client==0.32.2" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 59d9d6ddad8..3b30146e756 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.32.1"], + "requirements": ["async-upnp-client==0.32.2"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 9b4151c35c5..4c45b099193 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.32.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.32.2", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 16fe2ae7700..6c450548135 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.1"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.2"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 53966a90141..cabe62b50ec 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.13 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.32.1 +async-upnp-client==0.32.2 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ecbe0b8e6b8..d6473b7a5c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -353,7 +353,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.32.1 +async-upnp-client==0.32.2 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c594deaa475..a7d4b6bfb10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -307,7 +307,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.32.1 +async-upnp-client==0.32.2 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From b9757235a768efbd2f82a04996304b76277aacbf Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sat, 5 Nov 2022 21:26:19 +0100 Subject: [PATCH 21/50] Bump plugwise to v0.25.7 (#81612) --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 7f3e979ab7d..6bb1c941bf3 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.25.3"], + "requirements": ["plugwise==0.25.7"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index d6473b7a5c2..3c44a05c335 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1312,7 +1312,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.3 +plugwise==0.25.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7d4b6bfb10..40828d4f161 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -939,7 +939,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.3 +plugwise==0.25.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From ac15f2cf9dfc30f107c22a3c3797731b2dfaa1ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:15:16 -0600 Subject: [PATCH 22/50] Fix homekit bridge iid allocations (#81613) fixes undefined --- homeassistant/components/homekit/__init__.py | 10 +- .../components/homekit/accessories.py | 8 +- .../components/homekit/diagnostics.py | 2 + .../components/homekit/iidmanager.py | 72 ++++- tests/components/homekit/conftest.py | 8 +- tests/components/homekit/fixtures/iids_v1 | 249 ++++++++++++++++ .../homekit/fixtures/iids_v1_with_underscore | 50 ++++ tests/components/homekit/fixtures/iids_v2 | 273 ++++++++++++++++++ .../homekit/fixtures/iids_v2_with_underscore | 54 ++++ tests/components/homekit/test_accessories.py | 22 +- tests/components/homekit/test_diagnostics.py | 28 ++ tests/components/homekit/test_homekit.py | 6 +- tests/components/homekit/test_iidmanager.py | 83 +++++- 13 files changed, 806 insertions(+), 59 deletions(-) create mode 100644 tests/components/homekit/fixtures/iids_v1 create mode 100644 tests/components/homekit/fixtures/iids_v1_with_underscore create mode 100644 tests/components/homekit/fixtures/iids_v2 create mode 100644 tests/components/homekit/fixtures/iids_v2_with_underscore diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 333d6052d17..ca73c7dc242 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -76,13 +76,7 @@ from . import ( # noqa: F401 type_switches, type_thermostats, ) -from .accessories import ( - HomeAccessory, - HomeBridge, - HomeDriver, - HomeIIDManager, - get_accessory, -) +from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory from .aidmanager import AccessoryAidStorage from .const import ( ATTR_INTEGRATION, @@ -551,7 +545,7 @@ class HomeKit: async_zeroconf_instance=async_zeroconf_instance, zeroconf_server=f"{uuid}-hap.local.", loader=get_loader(), - iid_manager=HomeIIDManager(self.iid_storage), + iid_storage=self.iid_storage, ) # If we do not load the mac address will be wrong diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 61c2e3cd5dd..44b73b41ded 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -270,7 +270,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] driver=driver, display_name=cleanup_name_for_homekit(name), aid=aid, - iid_manager=driver.iid_manager, + iid_manager=HomeIIDManager(driver.iid_storage), *args, **kwargs, ) @@ -570,7 +570,7 @@ class HomeBridge(Bridge): # type: ignore[misc] def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None: """Initialize a Bridge object.""" - super().__init__(driver, name, iid_manager=driver.iid_manager) + super().__init__(driver, name, iid_manager=HomeIIDManager(driver.iid_storage)) self.set_info_service( firmware_revision=format_version(__version__), manufacturer=MANUFACTURER, @@ -603,7 +603,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc] entry_id: str, bridge_name: str, entry_title: str, - iid_manager: HomeIIDManager, + iid_storage: AccessoryIIDStorage, **kwargs: Any, ) -> None: """Initialize a AccessoryDriver object.""" @@ -612,7 +612,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc] self._entry_id = entry_id self._bridge_name = bridge_name self._entry_title = entry_title - self.iid_manager = iid_manager + self.iid_storage = iid_storage @pyhap_callback # type: ignore[misc] def pair( diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py index dbd40c1d6f5..1d0bfb92fcc 100644 --- a/homeassistant/components/homekit/diagnostics.py +++ b/homeassistant/components/homekit/diagnostics.py @@ -31,6 +31,8 @@ async def async_get_config_entry_diagnostics( "options": dict(entry.options), }, } + if homekit.iid_storage: + data["iid_storage"] = homekit.iid_storage.allocations if not homekit.driver: # not started yet or startup failed return data driver: AccessoryDriver = homekit.driver diff --git a/homeassistant/components/homekit/iidmanager.py b/homeassistant/components/homekit/iidmanager.py index 1b5cc7d6722..3805748225a 100644 --- a/homeassistant/components/homekit/iidmanager.py +++ b/homeassistant/components/homekit/iidmanager.py @@ -17,7 +17,7 @@ from homeassistant.helpers.storage import Store from .util import get_iid_storage_filename_for_entry_id -IID_MANAGER_STORAGE_VERSION = 1 +IID_MANAGER_STORAGE_VERSION = 2 IID_MANAGER_SAVE_DELAY = 2 ALLOCATIONS_KEY = "allocations" @@ -26,6 +26,40 @@ IID_MIN = 1 IID_MAX = 18446744073709551615 +ACCESSORY_INFORMATION_SERVICE = "3E" + + +class IIDStorage(Store): + """Storage class for IIDManager.""" + + async def _async_migrate_func( + self, + old_major_version: int, + old_minor_version: int, + old_data: dict, + ): + """Migrate to the new version.""" + if old_major_version == 1: + # Convert v1 to v2 format which uses a unique iid set per accessory + # instead of per pairing since we need the ACCESSORY_INFORMATION_SERVICE + # to always have iid 1 for each bridged accessory as well as the bridge + old_allocations: dict[str, int] = old_data.pop(ALLOCATIONS_KEY, {}) + new_allocation: dict[str, dict[str, int]] = {} + old_data[ALLOCATIONS_KEY] = new_allocation + for allocation_key, iid in old_allocations.items(): + aid_str, new_allocation_key = allocation_key.split("_", 1) + service_type, _, char_type, *_ = new_allocation_key.split("_") + accessory_allocation = new_allocation.setdefault(aid_str, {}) + if service_type == ACCESSORY_INFORMATION_SERVICE and not char_type: + accessory_allocation[new_allocation_key] = 1 + elif iid != 1: + accessory_allocation[new_allocation_key] = iid + + return old_data + + raise NotImplementedError + + class AccessoryIIDStorage: """ Provide stable allocation of IIDs for the lifetime of an accessory. @@ -37,15 +71,15 @@ class AccessoryIIDStorage: def __init__(self, hass: HomeAssistant, entry_id: str) -> None: """Create a new iid store.""" self.hass = hass - self.allocations: dict[str, int] = {} - self.allocated_iids: list[int] = [] + self.allocations: dict[str, dict[str, int]] = {} + self.allocated_iids: dict[str, list[int]] = {} self.entry_id = entry_id - self.store: Store | None = None + self.store: IIDStorage | None = None async def async_initialize(self) -> None: """Load the latest IID data.""" iid_store = get_iid_storage_filename_for_entry_id(self.entry_id) - self.store = Store(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store) + self.store = IIDStorage(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store) if not (raw_storage := await self.store.async_load()): # There is no data about iid allocations yet @@ -53,7 +87,8 @@ class AccessoryIIDStorage: assert isinstance(raw_storage, dict) self.allocations = raw_storage.get(ALLOCATIONS_KEY, {}) - self.allocated_iids = sorted(self.allocations.values()) + for aid_str, allocations in self.allocations.items(): + self.allocated_iids[aid_str] = sorted(allocations.values()) def get_or_allocate_iid( self, @@ -68,16 +103,25 @@ class AccessoryIIDStorage: char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None # Allocation key must be a string since we are saving it to JSON allocation_key = ( - f'{aid}_{service_hap_type}_{service_unique_id or ""}_' + f'{service_hap_type}_{service_unique_id or ""}_' f'{char_hap_type or ""}_{char_unique_id or ""}' ) - if allocation_key in self.allocations: - return self.allocations[allocation_key] - next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1 - self.allocations[allocation_key] = next_iid - self.allocated_iids.append(next_iid) + # AID must be a string since JSON keys cannot be int + aid_str = str(aid) + accessory_allocation = self.allocations.setdefault(aid_str, {}) + accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, []) + if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None: + allocated_iid = 1 + elif allocation_key in accessory_allocation: + return accessory_allocation[allocation_key] + elif accessory_allocated_iids: + allocated_iid = accessory_allocated_iids[-1] + 1 + else: + allocated_iid = 2 + accessory_allocation[allocation_key] = allocated_iid + accessory_allocated_iids.append(allocated_iid) self._async_schedule_save() - return next_iid + return allocated_iid @callback def _async_schedule_save(self) -> None: @@ -91,6 +135,6 @@ class AccessoryIIDStorage: return await self.store.async_save(self._data_to_save()) @callback - def _data_to_save(self) -> dict[str, dict[str, int]]: + def _data_to_save(self) -> dict[str, dict[str, dict[str, int]]]: """Return data of entity map to store in a file.""" return {ALLOCATIONS_KEY: self.allocations} diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 7b79e0f9b6b..b0422a40f72 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components.device_tracker.legacy import YAML_DEVICES -from homeassistant.components.homekit.accessories import HomeDriver, HomeIIDManager +from homeassistant.components.homekit.accessories import HomeDriver from homeassistant.components.homekit.const import BRIDGE_NAME, EVENT_HOMEKIT_CHANGED from homeassistant.components.homekit.iidmanager import AccessoryIIDStorage @@ -39,7 +39,7 @@ def run_driver(hass, loop, iid_storage): entry_id="", entry_title="mock entry", bridge_name=BRIDGE_NAME, - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address="127.0.0.1", loop=loop, ) @@ -63,7 +63,7 @@ def hk_driver(hass, loop, iid_storage): entry_id="", entry_title="mock entry", bridge_name=BRIDGE_NAME, - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address="127.0.0.1", loop=loop, ) @@ -91,7 +91,7 @@ def mock_hap(hass, loop, iid_storage, mock_zeroconf): entry_id="", entry_title="mock entry", bridge_name=BRIDGE_NAME, - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address="127.0.0.1", loop=loop, ) diff --git a/tests/components/homekit/fixtures/iids_v1 b/tests/components/homekit/fixtures/iids_v1 new file mode 100644 index 00000000000..1da11d8de67 --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v1 @@ -0,0 +1,249 @@ +{ + "version": 1, + "minor_version": 1, + "key": "homekit.v1.iids", + "data": { + "allocations": { + "1_3E___": 1, + "1_3E__14_": 2, + "1_3E__20_": 3, + "1_3E__21_": 4, + "1_3E__23_": 5, + "1_3E__30_": 6, + "1_3E__52_": 7, + "1_A2___": 8, + "1_A2__37_": 9, + "935391877_3E___": 10, + "935391877_3E__14_": 11, + "935391877_3E__20_": 12, + "935391877_3E__21_": 13, + "935391877_3E__23_": 14, + "935391877_3E__30_": 15, + "935391877_3E__52_": 16, + "935391877_4A___": 17, + "935391877_4A__F_": 18, + "935391877_4A__33_": 19, + "935391877_4A__11_": 20, + "935391877_4A__35_": 21, + "935391877_4A__36_": 22, + "935391877_4A__D_": 23, + "935391877_4A__12_": 24, + "935391877_4A__34_": 25, + "935391877_4A__10_": 26, + "935391877_B7___": 27, + "935391877_B7__B0_": 28, + "935391877_B7__BF_": 29, + "935391877_B7__AF_": 30, + "985724734_3E___": 31, + "985724734_3E__14_": 32, + "985724734_3E__20_": 33, + "985724734_3E__21_": 34, + "985724734_3E__23_": 35, + "985724734_3E__30_": 36, + "985724734_3E__52_": 37, + "985724734_4A___": 38, + "985724734_4A__F_": 39, + "985724734_4A__33_": 40, + "985724734_4A__11_": 41, + "985724734_4A__35_": 42, + "985724734_4A__36_": 43, + "985724734_4A__D_": 44, + "985724734_4A__12_": 45, + "985724734_4A__34_": 46, + "985724734_4A__10_": 47, + "985724734_B7___": 48, + "985724734_B7__B0_": 49, + "985724734_B7__BF_": 50, + "985724734_B7__AF_": 51, + "3083074204_3E___": 52, + "3083074204_3E__14_": 53, + "3083074204_3E__20_": 54, + "3083074204_3E__21_": 55, + "3083074204_3E__23_": 56, + "3083074204_3E__30_": 57, + "3083074204_3E__52_": 58, + "3083074204_4A___": 59, + "3083074204_4A__F_": 60, + "3083074204_4A__33_": 61, + "3083074204_4A__11_": 62, + "3083074204_4A__35_": 63, + "3083074204_4A__36_": 64, + "3083074204_4A__D_": 65, + "3083074204_4A__12_": 66, + "3083074204_4A__34_": 67, + "3083074204_4A__10_": 68, + "3083074204_B7___": 69, + "3083074204_B7__B0_": 70, + "3083074204_B7__BF_": 71, + "3083074204_B7__AF_": 72, + "3032741347_3E___": 73, + "3032741347_3E__14_": 74, + "3032741347_3E__20_": 75, + "3032741347_3E__21_": 76, + "3032741347_3E__23_": 77, + "3032741347_3E__30_": 78, + "3032741347_3E__52_": 79, + "3032741347_4A___": 80, + "3032741347_4A__F_": 81, + "3032741347_4A__33_": 82, + "3032741347_4A__11_": 83, + "3032741347_4A__35_": 84, + "3032741347_4A__36_": 85, + "3032741347_4A__D_": 86, + "3032741347_4A__12_": 87, + "3032741347_4A__34_": 88, + "3032741347_4A__10_": 89, + "3032741347_B7___": 90, + "3032741347_B7__B0_": 91, + "3032741347_B7__BF_": 92, + "3032741347_B7__AF_": 93, + "3681509609_3E___": 94, + "3681509609_3E__14_": 95, + "3681509609_3E__20_": 96, + "3681509609_3E__21_": 97, + "3681509609_3E__23_": 98, + "3681509609_3E__30_": 99, + "3681509609_3E__52_": 100, + "3681509609_4A___": 101, + "3681509609_4A__F_": 102, + "3681509609_4A__33_": 103, + "3681509609_4A__11_": 104, + "3681509609_4A__35_": 105, + "3681509609_4A__36_": 106, + "3681509609_4A__D_": 107, + "3681509609_4A__12_": 108, + "3681509609_4A__34_": 109, + "3681509609_4A__10_": 110, + "3681509609_B7___": 111, + "3681509609_B7__B0_": 112, + "3681509609_B7__BF_": 113, + "3681509609_B7__AF_": 114, + "3866063418_3E___": 115, + "3866063418_3E__14_": 116, + "3866063418_3E__20_": 117, + "3866063418_3E__21_": 118, + "3866063418_3E__23_": 119, + "3866063418_3E__30_": 120, + "3866063418_3E__52_": 121, + "3866063418_4A___": 122, + "3866063418_4A__F_": 123, + "3866063418_4A__33_": 124, + "3866063418_4A__11_": 125, + "3866063418_4A__35_": 126, + "3866063418_4A__36_": 127, + "3866063418_4A__D_": 128, + "3866063418_4A__12_": 129, + "3866063418_4A__34_": 130, + "3866063418_4A__10_": 131, + "3866063418_B7___": 132, + "3866063418_B7__B0_": 133, + "3866063418_B7__BF_": 134, + "3866063418_B7__AF_": 135, + "3239498961_3E___": 136, + "3239498961_3E__14_": 137, + "3239498961_3E__20_": 138, + "3239498961_3E__21_": 139, + "3239498961_3E__23_": 140, + "3239498961_3E__30_": 141, + "3239498961_3E__52_": 142, + "3239498961_4A___": 143, + "3239498961_4A__F_": 144, + "3239498961_4A__33_": 145, + "3239498961_4A__11_": 146, + "3239498961_4A__35_": 147, + "3239498961_4A__36_": 148, + "3239498961_4A__D_": 149, + "3239498961_4A__12_": 150, + "3239498961_4A__34_": 151, + "3239498961_4A__10_": 152, + "3239498961_B7___": 153, + "3239498961_B7__B0_": 154, + "3239498961_B7__BF_": 155, + "3239498961_B7__AF_": 156, + "3289831818_3E___": 157, + "3289831818_3E__14_": 158, + "3289831818_3E__20_": 159, + "3289831818_3E__21_": 160, + "3289831818_3E__23_": 161, + "3289831818_3E__30_": 162, + "3289831818_3E__52_": 163, + "3289831818_4A___": 164, + "3289831818_4A__F_": 165, + "3289831818_4A__33_": 166, + "3289831818_4A__11_": 167, + "3289831818_4A__35_": 168, + "3289831818_4A__36_": 169, + "3289831818_4A__D_": 170, + "3289831818_4A__12_": 171, + "3289831818_4A__34_": 172, + "3289831818_4A__10_": 173, + "3289831818_B7___": 174, + "3289831818_B7__B0_": 175, + "3289831818_B7__BF_": 176, + "3289831818_B7__AF_": 177, + "3071722771_3E___": 178, + "3071722771_3E__14_": 179, + "3071722771_3E__20_": 180, + "3071722771_3E__21_": 181, + "3071722771_3E__23_": 182, + "3071722771_3E__30_": 183, + "3071722771_3E__52_": 184, + "3071722771_4A___": 185, + "3071722771_4A__F_": 186, + "3071722771_4A__33_": 187, + "3071722771_4A__11_": 188, + "3071722771_4A__35_": 189, + "3071722771_4A__36_": 190, + "3071722771_4A__D_": 191, + "3071722771_4A__12_": 192, + "3071722771_4A__34_": 193, + "3071722771_4A__10_": 194, + "3071722771_B7___": 195, + "3071722771_B7__B0_": 196, + "3071722771_B7__BF_": 197, + "3071722771_B7__AF_": 198, + "3391630365_3E___": 199, + "3391630365_3E__14_": 200, + "3391630365_3E__20_": 201, + "3391630365_3E__21_": 202, + "3391630365_3E__23_": 203, + "3391630365_3E__30_": 204, + "3391630365_3E__52_": 205, + "3391630365_4A___": 206, + "3391630365_4A__F_": 207, + "3391630365_4A__33_": 208, + "3391630365_4A__11_": 209, + "3391630365_4A__35_": 210, + "3391630365_4A__36_": 211, + "3391630365_4A__D_": 212, + "3391630365_4A__12_": 213, + "3391630365_4A__34_": 214, + "3391630365_4A__10_": 215, + "3391630365_B7___": 216, + "3391630365_B7__B0_": 217, + "3391630365_B7__BF_": 218, + "3391630365_B7__AF_": 219, + "3274187032_3E___": 220, + "3274187032_3E__14_": 221, + "3274187032_3E__20_": 222, + "3274187032_3E__21_": 223, + "3274187032_3E__23_": 224, + "3274187032_3E__30_": 225, + "3274187032_3E__52_": 226, + "3274187032_4A___": 227, + "3274187032_4A__F_": 228, + "3274187032_4A__33_": 229, + "3274187032_4A__11_": 230, + "3274187032_4A__35_": 231, + "3274187032_4A__36_": 232, + "3274187032_4A__D_": 233, + "3274187032_4A__12_": 234, + "3274187032_4A__34_": 235, + "3274187032_4A__10_": 236, + "3274187032_B7___": 237, + "3274187032_B7__B0_": 238, + "3274187032_B7__BF_": 239, + "3274187032_B7__AF_": 240 + } + } +} diff --git a/tests/components/homekit/fixtures/iids_v1_with_underscore b/tests/components/homekit/fixtures/iids_v1_with_underscore new file mode 100644 index 00000000000..844c17a4746 --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v1_with_underscore @@ -0,0 +1,50 @@ +{ + "version": 1, + "minor_version": 1, + "key": "homekit.8a47205bd97c07d7a908f10166ebe636.iids", + "data": { + "allocations": { + "1_3E___": 1, + "1_3E__14_": 2, + "1_3E__20_": 3, + "1_3E__21_": 4, + "1_3E__23_": 5, + "1_3E__30_": 6, + "1_3E__52_": 7, + "1_A2___": 8, + "1_A2__37_": 9, + "1973560704_3E___": 10, + "1973560704_3E__14_": 11, + "1973560704_3E__20_": 12, + "1973560704_3E__21_": 13, + "1973560704_3E__23_": 14, + "1973560704_3E__30_": 15, + "1973560704_3E__52_": 16, + "1973560704_3E__53_": 17, + "1973560704_89_pressed-__": 18, + "1973560704_89_pressed-_73_": 19, + "1973560704_89_pressed-_23_": 20, + "1973560704_89_pressed-_CB_": 21, + "1973560704_CC_pressed-__": 22, + "1973560704_CC_pressed-_CD_": 23, + "1973560704_89_changed_states-__": 24, + "1973560704_89_changed_states-_73_": 25, + "1973560704_89_changed_states-_23_": 26, + "1973560704_89_changed_states-_CB_": 27, + "1973560704_CC_changed_states-__": 28, + "1973560704_CC_changed_states-_CD_": 29, + "1973560704_89_turned_off-__": 30, + "1973560704_89_turned_off-_73_": 31, + "1973560704_89_turned_off-_23_": 32, + "1973560704_89_turned_off-_CB_": 33, + "1973560704_CC_turned_off-__": 34, + "1973560704_CC_turned_off-_CD_": 35, + "1973560704_89_turned_on-__": 36, + "1973560704_89_turned_on-_73_": 37, + "1973560704_89_turned_on-_23_": 38, + "1973560704_89_turned_on-_CB_": 39, + "1973560704_CC_turned_on-__": 40, + "1973560704_CC_turned_on-_CD_": 41 + } + } +} \ No newline at end of file diff --git a/tests/components/homekit/fixtures/iids_v2 b/tests/components/homekit/fixtures/iids_v2 new file mode 100644 index 00000000000..76bff55e935 --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v2 @@ -0,0 +1,273 @@ +{ + "version": 2, + "minor_version": 1, + "key": "homekit.v2.iids", + "data": { + "allocations": { + "1": { + "3E___": 1, + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "A2___": 8, + "A2__37_": 9 + }, + "935391877": { + "3E___": 1, + "3E__14_": 11, + "3E__20_": 12, + "3E__21_": 13, + "3E__23_": 14, + "3E__30_": 15, + "3E__52_": 16, + "4A___": 17, + "4A__F_": 18, + "4A__33_": 19, + "4A__11_": 20, + "4A__35_": 21, + "4A__36_": 22, + "4A__D_": 23, + "4A__12_": 24, + "4A__34_": 25, + "4A__10_": 26, + "B7___": 27, + "B7__B0_": 28, + "B7__BF_": 29, + "B7__AF_": 30 + }, + "985724734": { + "3E___": 1, + "3E__14_": 32, + "3E__20_": 33, + "3E__21_": 34, + "3E__23_": 35, + "3E__30_": 36, + "3E__52_": 37, + "4A___": 38, + "4A__F_": 39, + "4A__33_": 40, + "4A__11_": 41, + "4A__35_": 42, + "4A__36_": 43, + "4A__D_": 44, + "4A__12_": 45, + "4A__34_": 46, + "4A__10_": 47, + "B7___": 48, + "B7__B0_": 49, + "B7__BF_": 50, + "B7__AF_": 51 + }, + "3083074204": { + "3E___": 1, + "3E__14_": 53, + "3E__20_": 54, + "3E__21_": 55, + "3E__23_": 56, + "3E__30_": 57, + "3E__52_": 58, + "4A___": 59, + "4A__F_": 60, + "4A__33_": 61, + "4A__11_": 62, + "4A__35_": 63, + "4A__36_": 64, + "4A__D_": 65, + "4A__12_": 66, + "4A__34_": 67, + "4A__10_": 68, + "B7___": 69, + "B7__B0_": 70, + "B7__BF_": 71, + "B7__AF_": 72 + }, + "3032741347": { + "3E___": 1, + "3E__14_": 74, + "3E__20_": 75, + "3E__21_": 76, + "3E__23_": 77, + "3E__30_": 78, + "3E__52_": 79, + "4A___": 80, + "4A__F_": 81, + "4A__33_": 82, + "4A__11_": 83, + "4A__35_": 84, + "4A__36_": 85, + "4A__D_": 86, + "4A__12_": 87, + "4A__34_": 88, + "4A__10_": 89, + "B7___": 90, + "B7__B0_": 91, + "B7__BF_": 92, + "B7__AF_": 93 + }, + "3681509609": { + "3E___": 1, + "3E__14_": 95, + "3E__20_": 96, + "3E__21_": 97, + "3E__23_": 98, + "3E__30_": 99, + "3E__52_": 100, + "4A___": 101, + "4A__F_": 102, + "4A__33_": 103, + "4A__11_": 104, + "4A__35_": 105, + "4A__36_": 106, + "4A__D_": 107, + "4A__12_": 108, + "4A__34_": 109, + "4A__10_": 110, + "B7___": 111, + "B7__B0_": 112, + "B7__BF_": 113, + "B7__AF_": 114 + }, + "3866063418": { + "3E___": 1, + "3E__14_": 116, + "3E__20_": 117, + "3E__21_": 118, + "3E__23_": 119, + "3E__30_": 120, + "3E__52_": 121, + "4A___": 122, + "4A__F_": 123, + "4A__33_": 124, + "4A__11_": 125, + "4A__35_": 126, + "4A__36_": 127, + "4A__D_": 128, + "4A__12_": 129, + "4A__34_": 130, + "4A__10_": 131, + "B7___": 132, + "B7__B0_": 133, + "B7__BF_": 134, + "B7__AF_": 135 + }, + "3239498961": { + "3E___": 1, + "3E__14_": 137, + "3E__20_": 138, + "3E__21_": 139, + "3E__23_": 140, + "3E__30_": 141, + "3E__52_": 142, + "4A___": 143, + "4A__F_": 144, + "4A__33_": 145, + "4A__11_": 146, + "4A__35_": 147, + "4A__36_": 148, + "4A__D_": 149, + "4A__12_": 150, + "4A__34_": 151, + "4A__10_": 152, + "B7___": 153, + "B7__B0_": 154, + "B7__BF_": 155, + "B7__AF_": 156 + }, + "3289831818": { + "3E___": 1, + "3E__14_": 158, + "3E__20_": 159, + "3E__21_": 160, + "3E__23_": 161, + "3E__30_": 162, + "3E__52_": 163, + "4A___": 164, + "4A__F_": 165, + "4A__33_": 166, + "4A__11_": 167, + "4A__35_": 168, + "4A__36_": 169, + "4A__D_": 170, + "4A__12_": 171, + "4A__34_": 172, + "4A__10_": 173, + "B7___": 174, + "B7__B0_": 175, + "B7__BF_": 176, + "B7__AF_": 177 + }, + "3071722771": { + "3E___": 1, + "3E__14_": 179, + "3E__20_": 180, + "3E__21_": 181, + "3E__23_": 182, + "3E__30_": 183, + "3E__52_": 184, + "4A___": 185, + "4A__F_": 186, + "4A__33_": 187, + "4A__11_": 188, + "4A__35_": 189, + "4A__36_": 190, + "4A__D_": 191, + "4A__12_": 192, + "4A__34_": 193, + "4A__10_": 194, + "B7___": 195, + "B7__B0_": 196, + "B7__BF_": 197, + "B7__AF_": 198 + }, + "3391630365": { + "3E___": 1, + "3E__14_": 200, + "3E__20_": 201, + "3E__21_": 202, + "3E__23_": 203, + "3E__30_": 204, + "3E__52_": 205, + "4A___": 206, + "4A__F_": 207, + "4A__33_": 208, + "4A__11_": 209, + "4A__35_": 210, + "4A__36_": 211, + "4A__D_": 212, + "4A__12_": 213, + "4A__34_": 214, + "4A__10_": 215, + "B7___": 216, + "B7__B0_": 217, + "B7__BF_": 218, + "B7__AF_": 219 + }, + "3274187032": { + "3E___": 1, + "3E__14_": 221, + "3E__20_": 222, + "3E__21_": 223, + "3E__23_": 224, + "3E__30_": 225, + "3E__52_": 226, + "4A___": 227, + "4A__F_": 228, + "4A__33_": 229, + "4A__11_": 230, + "4A__35_": 231, + "4A__36_": 232, + "4A__D_": 233, + "4A__12_": 234, + "4A__34_": 235, + "4A__10_": 236, + "B7___": 237, + "B7__B0_": 238, + "B7__BF_": 239, + "B7__AF_": 240 + } + } + } +} diff --git a/tests/components/homekit/fixtures/iids_v2_with_underscore b/tests/components/homekit/fixtures/iids_v2_with_underscore new file mode 100644 index 00000000000..52e874e41a5 --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v2_with_underscore @@ -0,0 +1,54 @@ +{ + "version": 2, + "minor_version": 1, + "key": "homekit.8a47205bd97c07d7a908f10166ebe636.iids", + "data": { + "allocations": { + "1": { + "3E___": 1, + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "A2___": 8, + "A2__37_": 9 + }, + "1973560704": { + "3E___": 1, + "3E__14_": 11, + "3E__20_": 12, + "3E__21_": 13, + "3E__23_": 14, + "3E__30_": 15, + "3E__52_": 16, + "3E__53_": 17, + "89_pressed-__": 18, + "89_pressed-_73_": 19, + "89_pressed-_23_": 20, + "89_pressed-_CB_": 21, + "CC_pressed-__": 22, + "CC_pressed-_CD_": 23, + "89_changed_states-__": 24, + "89_changed_states-_73_": 25, + "89_changed_states-_23_": 26, + "89_changed_states-_CB_": 27, + "CC_changed_states-__": 28, + "CC_changed_states-_CD_": 29, + "89_turned_off-__": 30, + "89_turned_off-_73_": 31, + "89_turned_off-_23_": 32, + "89_turned_off-_CB_": 33, + "CC_turned_off-__": 34, + "CC_turned_off-_CD_": 35, + "89_turned_on-__": 36, + "89_turned_on-_73_": 37, + "89_turned_on-_23_": 38, + "89_turned_on-_CB_": 39, + "CC_turned_on-__": 40, + "CC_turned_on-_CD_": 41 + } + } + } +} \ No newline at end of file diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 2a0f3f2f718..36eaeff91b4 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -10,7 +10,6 @@ from homeassistant.components.homekit.accessories import ( HomeAccessory, HomeBridge, HomeDriver, - HomeIIDManager, ) from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, @@ -724,7 +723,7 @@ def test_home_driver(iid_storage): "entry_id", "name", "title", - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address=ip_address, port=port, persist_file=path, @@ -752,22 +751,3 @@ def test_home_driver(iid_storage): mock_unpair.assert_called_with("client_uuid") mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0") - - -async def test_iid_collision_raises(hass, hk_driver): - """Test iid collision raises. - - If we try to allocate the same IID to the an accessory twice, we should - raise an exception. - """ - - entity_id = "light.accessory" - entity_id2 = "light.accessory2" - - hass.states.async_set(entity_id, STATE_OFF) - hass.states.async_set(entity_id2, STATE_OFF) - - HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, {}) - - with pytest.raises(RuntimeError): - HomeAccessory(hass, hk_driver, "Home Accessory", entity_id2, 2, {}) diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 1f6f7c584f3..be98c3bacdd 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -43,6 +43,19 @@ async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zer diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert diag == { "bridge": {}, + "iid_storage": { + "1": { + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "3E___": 1, + "A2__37_": 9, + "A2___": 8, + } + }, "accessories": [ { "aid": 1, @@ -257,6 +270,21 @@ async def test_config_entry_accessory( }, "config_version": 2, "pairing_id": ANY, + "iid_storage": { + "1": { + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "3E___": 1, + "43__25_": 11, + "43___": 10, + "A2__37_": 9, + "A2___": 8, + } + }, "status": 1, } with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 21dc94a4b54..d8c02aa98c4 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -262,7 +262,7 @@ async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance=zeroconf_mock, zeroconf_server=f"{uuid}-hap.local.", loader=ANY, - iid_manager=ANY, + iid_storage=ANY, ) assert homekit.driver.safe_mode is False @@ -306,7 +306,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance=mock_async_zeroconf, zeroconf_server=f"{uuid}-hap.local.", loader=ANY, - iid_manager=ANY, + iid_storage=ANY, ) @@ -350,7 +350,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance=async_zeroconf_instance, zeroconf_server=f"{uuid}-hap.local.", loader=ANY, - iid_manager=ANY, + iid_storage=ANY, ) diff --git a/tests/components/homekit/test_iidmanager.py b/tests/components/homekit/test_iidmanager.py index a791c30a341..3e4a19c9045 100644 --- a/tests/components/homekit/test_iidmanager.py +++ b/tests/components/homekit/test_iidmanager.py @@ -1,6 +1,5 @@ """Tests for the HomeKit IID manager.""" - from uuid import UUID from homeassistant.components.homekit.const import DOMAIN @@ -8,9 +7,10 @@ from homeassistant.components.homekit.iidmanager import ( AccessoryIIDStorage, get_iid_storage_filename_for_entry_id, ) +from homeassistant.helpers.json import json_loads from homeassistant.util.uuid import random_uuid_hex -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture async def test_iid_generation_and_restore(hass, iid_storage, hass_storage): @@ -77,9 +77,6 @@ async def test_iid_generation_and_restore(hass, iid_storage, hass_storage): unique_service_unique_char_new_aid_iid1 == unique_service_unique_char_new_aid_iid2 ) - assert unique_service_unique_char_new_aid_iid1 != iid1 - assert unique_service_unique_char_new_aid_iid1 != unique_service_unique_char_iid1 - await iid_storage.async_save() iid_storage2 = AccessoryIIDStorage(hass, entry.entry_id) @@ -99,3 +96,79 @@ async def test_iid_storage_filename(hass, iid_storage, hass_storage): assert iid_storage.store.path.endswith( get_iid_storage_filename_for_entry_id(entry.entry_id) ) + + +async def test_iid_migration_to_v2(hass, iid_storage, hass_storage): + """Test iid storage migration.""" + v1_iids = json_loads(load_fixture("iids_v1", DOMAIN)) + v2_iids = json_loads(load_fixture("iids_v2", DOMAIN)) + hass_storage["homekit.v1.iids"] = v1_iids + hass_storage["homekit.v2.iids"] = v2_iids + + iid_storage_v2 = AccessoryIIDStorage(hass, "v1") + await iid_storage_v2.async_initialize() + + iid_storage_v1 = AccessoryIIDStorage(hass, "v2") + await iid_storage_v1.async_initialize() + + assert iid_storage_v1.allocations == iid_storage_v2.allocations + assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids + + assert len(iid_storage_v2.allocations) == 12 + + for allocations in iid_storage_v2.allocations.values(): + assert allocations["3E___"] == 1 + + +async def test_iid_migration_to_v2_with_underscore(hass, iid_storage, hass_storage): + """Test iid storage migration with underscore.""" + v1_iids = json_loads(load_fixture("iids_v1_with_underscore", DOMAIN)) + v2_iids = json_loads(load_fixture("iids_v2_with_underscore", DOMAIN)) + hass_storage["homekit.v1_with_underscore.iids"] = v1_iids + hass_storage["homekit.v2_with_underscore.iids"] = v2_iids + + iid_storage_v2 = AccessoryIIDStorage(hass, "v1_with_underscore") + await iid_storage_v2.async_initialize() + + iid_storage_v1 = AccessoryIIDStorage(hass, "v2_with_underscore") + await iid_storage_v1.async_initialize() + + assert iid_storage_v1.allocations == iid_storage_v2.allocations + assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids + + assert len(iid_storage_v2.allocations) == 2 + + for allocations in iid_storage_v2.allocations.values(): + assert allocations["3E___"] == 1 + + +async def test_iid_generation_and_restore_v2(hass, iid_storage, hass_storage): + """Test generating iids and restoring them from storage.""" + entry = MockConfigEntry(domain=DOMAIN) + + iid_storage = AccessoryIIDStorage(hass, entry.entry_id) + await iid_storage.async_initialize() + not_accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 1, "000000AA-0000-1000-8000-0026BB765291", None, None, None + ) + assert not_accessory_info_service_iid == 2 + not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( + 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None + ) + assert not_accessory_info_service_iid_2 == 3 + not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( + 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None + ) + assert not_accessory_info_service_iid_2 == 3 + accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None + ) + assert accessory_info_service_iid == 1 + accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None + ) + assert accessory_info_service_iid == 1 + accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 2, "0000003E-0000-1000-8000-0026BB765291", None, None, None + ) + assert accessory_info_service_iid == 1 From c60c99bd74821f61114730e5c3e0aebb1f410865 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Sat, 5 Nov 2022 13:29:20 -0700 Subject: [PATCH 23/50] Bump venstarcolortouch to 0.19 to fix API rev 3 devices (#81614) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 4a6eea28e24..ce40e53105a 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.18"], + "requirements": ["venstarcolortouch==0.19"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index 3c44a05c335..1d52708eacb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2487,7 +2487,7 @@ vehicle==0.4.0 velbus-aio==2022.10.4 # homeassistant.components.venstar -venstarcolortouch==0.18 +venstarcolortouch==0.19 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40828d4f161..b0955ae7b62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1721,7 +1721,7 @@ vehicle==0.4.0 velbus-aio==2022.10.4 # homeassistant.components.venstar -venstarcolortouch==0.18 +venstarcolortouch==0.19 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From 4391640734df8d8825b0d84bc2a19af33b38f9dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 15:50:45 -0600 Subject: [PATCH 24/50] Ignore unspecified addresses from zeroconf (#81620) --- homeassistant/components/zeroconf/__init__.py | 12 ++++++++++-- tests/components/zeroconf/test_init.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 62783e641d3..82a9604a08c 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -552,12 +552,20 @@ def _first_non_link_local_address( """Return the first ipv6 or non-link local ipv4 address, preferring IPv4.""" for address in addresses: ip_addr = ip_address(address) - if not ip_addr.is_link_local and ip_addr.version == 4: + if ( + not ip_addr.is_link_local + and not ip_addr.is_unspecified + and ip_addr.version == 4 + ): return str(ip_addr) # If we didn't find a good IPv4 address, check for IPv6 addresses. for address in addresses: ip_addr = ip_address(address) - if not ip_addr.is_link_local and ip_addr.version == 6: + if ( + not ip_addr.is_link_local + and not ip_addr.is_unspecified + and ip_addr.version == 6 + ): return str(ip_addr) return None diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 039672b9955..a0684d6e10e 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -819,6 +819,24 @@ async def test_info_from_service_with_link_local_address_first(hass): assert info.host == "192.168.66.12" +async def test_info_from_service_with_unspecified_address_first(hass): + """Test that the unspecified address is ignored.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["0.0.0.0", "192.168.66.12"] + info = zeroconf.info_from_service(service_info) + assert info.host == "192.168.66.12" + + +async def test_info_from_service_with_unspecified_address_only(hass): + """Test that the unspecified address is ignored.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["0.0.0.0"] + info = zeroconf.info_from_service(service_info) + assert info is None + + async def test_info_from_service_with_link_local_address_second(hass): """Test that the link local address is ignored.""" service_type = "_test._tcp.local." From 6fa69022f4fe3e6eb85f091636484a11d2c7feac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 15:58:11 -0500 Subject: [PATCH 25/50] Bump aiohomekit to 2.2.16 (#81621) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index b2aec75c3ad..e4a2b5d79bb 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.14"], + "requirements": ["aiohomekit==2.2.16"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 1d52708eacb..f929c7f2d22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.14 +aiohomekit==2.2.16 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0955ae7b62..e106bfe0331 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.14 +aiohomekit==2.2.16 # homeassistant.components.emulated_hue # homeassistant.components.http From 6fb5c93182f34b9a207c93e10e48d1c02bcd0e9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 05:38:01 -0600 Subject: [PATCH 26/50] Bump oralb-ble to 0.13.0 (#81622) * Bump oralb-ble to 0.11.1 adds some more missing pressure mappings changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.2...v0.11.1 * bump again to update for additional reports * bump again for more data from issue reports --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index ba89c73a240..1738558770e 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.10.2"], + "requirements": ["oralb-ble==0.13.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index f929c7f2d22..b58f8a61743 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.10.2 +oralb-ble==0.13.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e106bfe0331..8d8863832e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.10.2 +oralb-ble==0.13.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From e4269ff8b20c942c0000c584495780f50ce0a76f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 05:09:36 -0600 Subject: [PATCH 27/50] Fix creating multiple ElkM1 systems with TLS 1.2 (#81627) fixes https://github.com/home-assistant/core/issues/81516 --- homeassistant/components/elkm1/__init__.py | 9 +- homeassistant/components/elkm1/config_flow.py | 13 +- tests/components/elkm1/test_config_flow.py | 146 ++++++++++++++++++ 3 files changed, 159 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 7833bfd66b3..7b14c7e85ed 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -7,11 +7,11 @@ import logging import re from types import MappingProxyType from typing import Any, cast -from urllib.parse import urlparse import async_timeout from elkm1_lib.elements import Element from elkm1_lib.elk import Elk +from elkm1_lib.util import parse_url import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -96,6 +96,11 @@ SET_TIME_SERVICE_SCHEMA = vol.Schema( ) +def hostname_from_url(url: str) -> str: + """Return the hostname from a url.""" + return parse_url(url)[1] + + def _host_validator(config: dict[str, str]) -> dict[str, str]: """Validate that a host is properly configured.""" if config[CONF_HOST].startswith("elks://"): @@ -231,7 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elk-M1 Control from a config entry.""" conf: MappingProxyType[str, Any] = entry.data - host = urlparse(entry.data[CONF_HOST]).hostname + host = hostname_from_url(entry.data[CONF_HOST]) _LOGGER.debug("Setting up elkm1 %s", conf["host"]) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 8675ff45ee7..ac7fc903330 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio import logging from typing import Any -from urllib.parse import urlparse from elkm1_lib.discovery import ElkSystem from elkm1_lib.elk import Elk @@ -26,7 +25,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import slugify from homeassistant.util.network import is_ip_address -from . import async_wait_for_elk_to_sync +from . import async_wait_for_elk_to_sync, hostname_from_url from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT from .discovery import ( _short_mac, @@ -170,7 +169,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for entry in self._async_current_entries(include_ignore=False): if ( entry.unique_id == mac - or urlparse(entry.data[CONF_HOST]).hostname == host + or hostname_from_url(entry.data[CONF_HOST]) == host ): if async_update_entry_from_discovery(self.hass, entry, device): self.hass.async_create_task( @@ -214,7 +213,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): current_unique_ids = self._async_current_ids() current_hosts = { - urlparse(entry.data[CONF_HOST]).hostname + hostname_from_url(entry.data[CONF_HOST]) for entry in self._async_current_entries(include_ignore=False) } discovered_devices = await async_discover_devices( @@ -344,7 +343,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._url_already_configured(url): return self.async_abort(reason="address_already_configured") - host = urlparse(url).hostname + host = hostname_from_url(url) _LOGGER.debug( "Importing is trying to fill unique id from discovery for %s", host ) @@ -367,10 +366,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _url_already_configured(self, url: str) -> bool: """See if we already have a elkm1 matching user input configured.""" existing_hosts = { - urlparse(entry.data[CONF_HOST]).hostname + hostname_from_url(entry.data[CONF_HOST]) for entry in self._async_current_entries() } - return urlparse(url).hostname in existing_hosts + return hostname_from_url(url) in existing_hosts class InvalidAuth(exceptions.HomeAssistantError): diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 7ce0e2163ac..a58528b700a 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1454,3 +1454,149 @@ async def test_multiple_instances_with_discovery(hass): "password": "", } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_multiple_instances_with_tls_v12(hass): + """Test we can setup a secure elk with tls v1_2.""" + + elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601) + elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601) + + with _patch_discovery(device=elk_discovery_1): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_1.mac_address}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert not result["errors"] + assert result2["step_id"] == "discovered_connection" + with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "protocol": "TLS 1.2", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeeff" + assert result3["data"] == { + "auto_configure": True, + "host": "elksv1_2://127.0.0.1", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # Now try to add another instance with the different discovery info + with _patch_discovery(device=elk_discovery_2): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_2.mac_address}, + ) + await hass.async_block_till_done() + + with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "protocol": "TLS 1.2", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeefe" + assert result3["data"] == { + "auto_configure": True, + "host": "elksv1_2://127.0.0.2", + "password": "test-password", + "prefix": "ddeefe", + "username": "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 + + # Finally, try to add another instance manually with no discovery info + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "TLS 1.2", + "address": "1.2.3.4", + "prefix": "guest_house", + "password": "test-password", + "username": "test-username", + }, + ) + await hass.async_block_till_done() + + import pprint + + pprint.pprint(result2) + assert result2["type"] == "create_entry" + assert result2["title"] == "guest_house" + assert result2["data"] == { + "auto_configure": True, + "host": "elksv1_2://1.2.3.4", + "prefix": "guest_house", + "password": "test-password", + "username": "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 From c8981f78b7abc0cba17700712b55dbf88a19bffc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 Nov 2022 21:23:36 +0100 Subject: [PATCH 28/50] Fix situation where deCONZ sensor platform setup would fail (#81629) * Fix situation where deCONZ sensor platform setup would fail * Don't use try --- homeassistant/components/deconz/sensor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 66c186e20d7..f1bd0118030 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -89,6 +89,7 @@ T = TypeVar( class DeconzSensorDescriptionMixin(Generic[T]): """Required values when describing secondary sensor attributes.""" + supported_fn: Callable[[T], bool] update_key: str value_fn: Callable[[T], datetime | StateType] @@ -105,6 +106,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( DeconzSensorDescription[AirQuality]( key="air_quality", + supported_fn=lambda device: device.air_quality is not None, update_key="airquality", value_fn=lambda device: device.air_quality, instance_check=AirQuality, @@ -112,6 +114,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[AirQuality]( key="air_quality_ppb", + supported_fn=lambda device: device.air_quality_ppb is not None, update_key="airqualityppb", value_fn=lambda device: device.air_quality_ppb, instance_check=AirQuality, @@ -122,6 +125,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Consumption]( key="consumption", + supported_fn=lambda device: device.consumption is not None, update_key="consumption", value_fn=lambda device: device.scaled_consumption, instance_check=Consumption, @@ -131,6 +135,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Daylight]( key="daylight_status", + supported_fn=lambda device: True, update_key="status", value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status], instance_check=Daylight, @@ -139,12 +144,14 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[GenericStatus]( key="status", + supported_fn=lambda device: device.status is not None, update_key="status", value_fn=lambda device: device.status, instance_check=GenericStatus, ), DeconzSensorDescription[Humidity]( key="humidity", + supported_fn=lambda device: device.humidity is not None, update_key="humidity", value_fn=lambda device: device.scaled_humidity, instance_check=Humidity, @@ -154,6 +161,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[LightLevel]( key="light_level", + supported_fn=lambda device: device.light_level is not None, update_key="lightlevel", value_fn=lambda device: device.scaled_light_level, instance_check=LightLevel, @@ -163,6 +171,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Power]( key="power", + supported_fn=lambda device: device.power is not None, update_key="power", value_fn=lambda device: device.power, instance_check=Power, @@ -172,6 +181,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Pressure]( key="pressure", + supported_fn=lambda device: device.pressure is not None, update_key="pressure", value_fn=lambda device: device.pressure, instance_check=Pressure, @@ -181,6 +191,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Temperature]( key="temperature", + supported_fn=lambda device: device.temperature is not None, update_key="temperature", value_fn=lambda device: device.scaled_temperature, instance_check=Temperature, @@ -190,6 +201,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Time]( key="last_set", + supported_fn=lambda device: device.last_set is not None, update_key="lastset", value_fn=lambda device: dt_util.parse_datetime(device.last_set), instance_check=Time, @@ -197,6 +209,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[SensorResources]( key="battery", + supported_fn=lambda device: device.battery is not None, update_key="battery", value_fn=lambda device: device.battery, name_suffix="Battery", @@ -208,6 +221,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[SensorResources]( key="internal_temperature", + supported_fn=lambda device: device.internal_temperature is not None, update_key="temperature", value_fn=lambda device: device.internal_temperature, name_suffix="Temperature", @@ -268,7 +282,7 @@ async def async_setup_entry( continue no_sensor_data = False - if description.value_fn(sensor) is None: + if not description.supported_fn(sensor): no_sensor_data = True if description.instance_check is None: From 08debee94fd6052e15b7ed57476cc644172e6ec4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 12:59:32 -0600 Subject: [PATCH 29/50] Add missing h2 dep to iaqualink (#81630) fixes https://github.com/home-assistant/core/issues/81439 --- homeassistant/components/iaqualink/manifest.json | 2 +- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index d5b7d7de0d8..f274cd5ea1c 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.5.0"], + "requirements": ["iaqualink==0.5.0", "h2==4.1.0"], "iot_class": "cloud_polling", "loggers": ["iaqualink"] } diff --git a/requirements_all.txt b/requirements_all.txt index b58f8a61743..dcefb2ba7fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -815,6 +815,9 @@ gstreamer-player==1.1.2 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.iaqualink +h2==4.1.0 + # homeassistant.components.homekit ha-HAP-python==4.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d8863832e5..d46e2ae375a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,6 +607,9 @@ gspread==5.5.0 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.iaqualink +h2==4.1.0 + # homeassistant.components.homekit ha-HAP-python==4.5.2 From f861137de4700fec31274750a059619151540faa Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 6 Nov 2022 10:21:48 +0100 Subject: [PATCH 30/50] Bump pyatmo to 7.4.0 (#81636) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index e34156ff589..0d4681197bd 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.3.0"], + "requirements": ["pyatmo==7.4.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index dcefb2ba7fd..f4f4df8e8a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1445,7 +1445,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.3.0 +pyatmo==7.4.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d46e2ae375a..8a647e1cae6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1033,7 +1033,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.3.0 +pyatmo==7.4.0 # homeassistant.components.apple_tv pyatv==0.10.3 From 63afb30f57ff24072c48d1f6fb55cd4bc95f7e57 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Sun, 6 Nov 2022 23:26:40 +0300 Subject: [PATCH 31/50] Fix Bravia TV options flow when device is off (#81644) * Fix options flow when tv is off * abort with message --- homeassistant/components/braviatv/config_flow.py | 6 +++++- homeassistant/components/braviatv/strings.json | 3 +++ homeassistant/components/braviatv/translations/en.json | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index f5c7826b825..45a2bad0036 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -262,7 +262,11 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): self.config_entry.entry_id ] - await coordinator.async_update_sources() + try: + await coordinator.async_update_sources() + except BraviaTVError: + return self.async_abort(reason="failed_update") + sources = coordinator.source_map.values() self.source_list = [item["title"] for item in sources] return await self.async_step_user() diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index 4dd08135896..ac651955166 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -48,6 +48,9 @@ "ignored_sources": "List of ignored sources" } } + }, + "abort": { + "failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up." } } } diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index c3341d33112..39c95c706de 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up." + }, "step": { "user": { "data": { From 2684a6e8eda6df4da469ae405da3f79ffba1a35a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 09:47:07 -0600 Subject: [PATCH 32/50] Bump aiohomekit to 2.2.17 (#81657) Improve BLE pairing reliability, especially with esp32 proxies changelog: https://github.com/Jc2k/aiohomekit/compare/2.2.16...2.2.17 --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index e4a2b5d79bb..7a088993e9f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.16"], + "requirements": ["aiohomekit==2.2.17"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index f4f4df8e8a5..462faa1d813 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.16 +aiohomekit==2.2.17 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a647e1cae6..efe5a96b00d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.16 +aiohomekit==2.2.17 # homeassistant.components.emulated_hue # homeassistant.components.http From 11013bd78090ebc1beca17fd7f88934e6f3f7cd5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 6 Nov 2022 12:38:55 -0700 Subject: [PATCH 33/50] Fix missing RainMachine restrictions switches (#81673) --- homeassistant/components/rainmachine/switch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index db560c3c64c..39087f36660 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -237,6 +237,7 @@ async def async_setup_entry( # Add switches to control restrictions: for description in RESTRICTIONS_SWITCH_DESCRIPTIONS: + coordinator = data.coordinators[description.api_category] if not key_exists(coordinator.data, description.data_key): continue entities.append(RainMachineRestrictionSwitch(entry, data, description)) From a8a3f012f66e2a61928c33d63342ed1a0adc3bdd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 04:44:05 -0500 Subject: [PATCH 34/50] Bump bluetooth-adapters to 0.7.0 (#81576) changelog: https://github.com/Bluetooth-Devices/bluetooth-adapters/releases/tag/v0.7.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 89281323541..93aabb3cc26 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -8,7 +8,7 @@ "requirements": [ "bleak==0.19.1", "bleak-retry-connector==2.8.2", - "bluetooth-adapters==0.6.0", + "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.61.1" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cabe62b50ec..f57a2affd6a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.8.2 bleak==0.19.1 -bluetooth-adapters==0.6.0 +bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 462faa1d813..48437186a5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,7 +438,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.6.0 +bluetooth-adapters==0.7.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index efe5a96b00d..96d2ed5a20b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.6.0 +bluetooth-adapters==0.7.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.6 From c8ed3fd302cd8e544b0382256861f3484c8e512e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 17:45:53 -0600 Subject: [PATCH 35/50] Bump bleak-retry-connector to 2.8.3 (#81675) Improves chances of making a BLE connection with the ESP32s changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.2...v2.8.3 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 93aabb3cc26..0569bc68e87 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.8.2", + "bleak-retry-connector==2.8.3", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.61.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f57a2affd6a..b25e684fe8c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.2 +bleak-retry-connector==2.8.3 bleak==0.19.1 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 48437186a5f..2b712cf48c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.2 +bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96d2ed5a20b..16e43e8fc24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.2 +bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth bleak==0.19.1 From 7ca5bd341bed081c4b6e4c7db8200e78e5a8d685 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 17:13:14 -0600 Subject: [PATCH 36/50] Bump aioesphomeapi to 11.4.3 (#81676) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 64cd6b4029c..2070b8ae362 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==11.4.2"], + "requirements": ["aioesphomeapi==11.4.3"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 2b712cf48c2..ae8bd698262 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.2 +aioesphomeapi==11.4.3 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16e43e8fc24..2819bfcbb6b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.2 +aioesphomeapi==11.4.3 # homeassistant.components.flo aioflo==2021.11.0 From efb984aa83c18f881b0e03c4ae84573c46cf7f24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 19:07:21 -0600 Subject: [PATCH 37/50] Bump bleak to 0.19.2 (#81688) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 0569bc68e87..2e038cda76e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.19.1", + "bleak==0.19.2", "bleak-retry-connector==2.8.3", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b25e684fe8c..ebf4eff0d2b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.8.3 -bleak==0.19.1 +bleak==0.19.2 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index ae8bd698262..b77191f095f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -416,7 +416,7 @@ bizkaibus==0.1.1 bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth -bleak==0.19.1 +bleak==0.19.2 # homeassistant.components.blebox blebox_uniapi==2.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2819bfcbb6b..8bcae988264 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -340,7 +340,7 @@ bimmer_connected==0.10.4 bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth -bleak==0.19.1 +bleak==0.19.2 # homeassistant.components.blebox blebox_uniapi==2.1.3 From 34ae83b4e2714b1730b52ef473d8ae32d93a3233 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 7 Nov 2022 01:16:58 +0100 Subject: [PATCH 38/50] Restore negative values for shelly power factors (#81689) fixes undefined --- homeassistant/components/shelly/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 922a5ac8b5c..7d99f015c5e 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -140,7 +140,7 @@ SENSORS: Final = { key="emeter|powerFactor", name="Power Factor", native_unit_of_measurement=PERCENTAGE, - value=lambda value: abs(round(value * 100, 1)), + value=lambda value: round(value * 100, 1), device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), From 823ec88c520b0b549bd6c980721d628a4b07b9a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 18:04:45 -0600 Subject: [PATCH 39/50] Bump aiohomekit to 2.2.18 (#81693) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 7a088993e9f..b18f35390b7 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.17"], + "requirements": ["aiohomekit==2.2.18"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index b77191f095f..c95d3258fcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.17 +aiohomekit==2.2.18 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8bcae988264..487595d4e07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.17 +aiohomekit==2.2.18 # homeassistant.components.emulated_hue # homeassistant.components.http From b9db84ed57948402b5c222865989f6619621a693 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 7 Nov 2022 06:23:49 -0500 Subject: [PATCH 40/50] Bump aiopyarr to 22.11.0 (#81694) --- homeassistant/components/lidarr/manifest.json | 2 +- homeassistant/components/radarr/manifest.json | 2 +- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json index 4c07e0e1762..eab24ef7e42 100644 --- a/homeassistant/components/lidarr/manifest.json +++ b/homeassistant/components/lidarr/manifest.json @@ -2,7 +2,7 @@ "domain": "lidarr", "name": "Lidarr", "documentation": "https://www.home-assistant.io/integrations/lidarr", - "requirements": ["aiopyarr==22.10.0"], + "requirements": ["aiopyarr==22.11.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 9b140def96a..5117fd161d3 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,7 +2,7 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "requirements": ["aiopyarr==22.10.0"], + "requirements": ["aiopyarr==22.11.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index daf9e20586b..511842a3e99 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.10.0"], + "requirements": ["aiopyarr==22.11.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index c95d3258fcf..a9d1c292018 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.10.0 +aiopyarr==22.11.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 487595d4e07..7d3fa0e0698 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.10.0 +aiopyarr==22.11.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 From 3e8bea8fbd592aa766a14287d4404afe43bd08c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 04:41:02 -0600 Subject: [PATCH 41/50] Fix flapping logbook tests (#81695) --- .../components/logbook/test_websocket_api.py | 91 ++++++++++++++----- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index fb0defca93c..42f604b3d4c 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + EVENT_HOMEASSISTANT_FINAL_WRITE, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, @@ -52,6 +53,15 @@ def set_utc(hass): hass.config.set_time_zone("UTC") +def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]: + """Return listeners without final write listeners since we are not testing for these.""" + return { + key: value + for key, value in listeners.items() + if key != EVENT_HOMEASSISTANT_FINAL_WRITE + } + + async def _async_mock_logbook_platform(hass: HomeAssistant) -> None: class MockLogbookPlatform: """Mock a logbook platform.""" @@ -684,7 +694,9 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -892,7 +904,9 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1083,7 +1097,9 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1386,7 +1402,9 @@ async def test_subscribe_unsubscribe_logbook_stream( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1484,7 +1502,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1586,12 +1606,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( assert msg["success"] # Check our listener got unsubscribed - listeners = hass.bus.async_listeners() - # The async_fire_time_changed above triggers unsubscribe from - # homeassistant_final_write, don't worry about those - init_listeners.pop("homeassistant_final_write") - listeners.pop("homeassistant_final_write") - assert listeners == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1659,7 +1676,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1759,7 +1778,9 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1853,7 +1874,9 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) async def test_event_stream_bad_start_time(recorder_mock, hass, hass_ws_client): @@ -1968,7 +1991,9 @@ async def test_logbook_stream_match_multiple_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) async def test_event_stream_bad_end_time(recorder_mock, hass, hass_ws_client): @@ -2091,7 +2116,9 @@ async def test_live_stream_with_one_second_commit_interval( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2146,7 +2173,9 @@ async def test_subscribe_disconnected(recorder_mock, hass, hass_ws_client): await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2189,7 +2218,9 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie assert msg["type"] == TYPE_RESULT assert msg["success"] - assert hass.bus.async_listeners() != init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) != listeners_without_writes(init_listeners) for _ in range(5): hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -2197,9 +2228,13 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie # Check our listener got unsubscribed because # the queue got full and the overload safety tripped - assert hass.bus.async_listeners() == after_ws_created_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(after_ws_created_listeners) await websocket_client.close() - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2332,7 +2367,9 @@ async def test_subscribe_all_entities_are_continuous( await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2494,7 +2531,9 @@ async def test_subscribe_entities_some_have_uom_multiple( await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2608,7 +2647,9 @@ async def test_logbook_stream_ignores_forced_updates( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2703,4 +2744,6 @@ async def test_subscribe_all_entities_are_continuous_with_device( await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) From 797ea3ace496ffa73f3085beaf9cdbf326b8a7a9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Nov 2022 11:18:58 +0100 Subject: [PATCH 42/50] Adjust REST schema validation (#81723) fixes undefined --- homeassistant/components/rest/schema.py | 9 ++++++++- tests/components/rest/test_init.py | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index f881dc8b028..d124ce5789c 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -89,6 +89,13 @@ COMBINED_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])}, + { + DOMAIN: vol.All( + # convert empty dict to empty list + lambda x: [] if x == {} else x, + cv.ensure_list, + [COMBINED_SCHEMA], + ) + }, extra=vol.ALLOW_EXTRA, ) diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 988f88b348e..b6861a098b1 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -18,7 +18,11 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, get_fixture_path +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + get_fixture_path, +) @respx.mock @@ -399,3 +403,17 @@ async def test_multiple_rest_endpoints(hass): assert hass.states.get("sensor.json_date_time").state == "07:11:08 PM" assert hass.states.get("sensor.json_time").state == "07:11:39 PM" assert hass.states.get("binary_sensor.binary_sensor").state == "on" + + +async def test_empty_config(hass: HomeAssistant) -> None: + """Test setup with empty configuration. + + For example (with rest.yaml an empty file): + rest: !include rest.yaml + """ + assert await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {}}, + ) + assert_setup_component(0, DOMAIN) From 1f878433ace40a44d627613cca15e70a95d803d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 21:19:57 -0600 Subject: [PATCH 43/50] Fix check for duplicate config entry reauth when context is passed or augmented (#81753) fixes https://github.com/home-assistant/core/issues/77578 --- homeassistant/config_entries.py | 29 +++++++++++++++-------------- tests/test_config_entries.py | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8e618deb3d2..b7ee9a4d654 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -660,24 +660,25 @@ class ConfigEntry: data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" - flow_context = { - "source": SOURCE_REAUTH, - "entry_id": self.entry_id, - "title_placeholders": {"name": self.title}, - "unique_id": self.unique_id, - } - - if context: - flow_context.update(context) - - for flow in hass.config_entries.flow.async_progress_by_handler(self.domain): - if flow["context"] == flow_context: - return + if any( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["context"].get("source") == SOURCE_REAUTH + and flow["context"].get("entry_id") == self.entry_id + ): + # Reauth flow already in progress for this entry + return hass.async_create_task( hass.config_entries.flow.async_init( self.domain, - context=flow_context, + context={ + "source": SOURCE_REAUTH, + "entry_id": self.entry_id, + "title_placeholders": {"name": self.title}, + "unique_id": self.unique_id, + } + | (context or {}), data=self.data | (data or {}), ) ) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 83343146d47..99e26be6d75 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3287,6 +3287,7 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): async def test_reauth(hass): """Test the async_reauth_helper.""" entry = MockConfigEntry(title="test_title", domain="test") + entry2 = MockConfigEntry(title="test_title", domain="test") mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) @@ -3313,7 +3314,19 @@ async def test_reauth(hass): assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234 + assert entry.entry_id != entry2.entry_id + # Check we can't start duplicate flows entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() - assert len(flows) == 1 + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Check we can't start duplicate when the context context is different + entry.async_start_reauth(hass, {"diff": "diff"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Check we can start a reauth for a different entry + entry2.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 2 From 815249eaeb03a3fda8cc8e35d14f31359581596d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:20:54 -0600 Subject: [PATCH 44/50] Use more efficient async_progress_by_handler call in async_start_reauth (#81757) --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b7ee9a4d654..bd985517ca7 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -662,7 +662,7 @@ class ConfigEntry: """Start a reauth flow.""" if any( flow - for flow in hass.config_entries.flow.async_progress() + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain) if flow["context"].get("source") == SOURCE_REAUTH and flow["context"].get("entry_id") == self.entry_id ): From f614df29bd1abd170f85a745759f9222b541b338 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 8 Nov 2022 02:21:13 -0800 Subject: [PATCH 45/50] Partially revert google local sync for search cases (#81761) fixes undefined --- homeassistant/components/google/calendar.py | 154 +++++++++++++++----- 1 file changed, 118 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index ca1228759cd..4eb57cff49c 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -3,11 +3,12 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from datetime import datetime, timedelta import logging from typing import Any -from gcal_sync.api import SyncEventsRequest +from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest from gcal_sync.exceptions import ApiException from gcal_sync.model import DateOrDatetime, Event from gcal_sync.store import ScopedCalendarStore @@ -196,21 +197,30 @@ async def async_setup_entry( entity_registry.async_remove( entity_entry.entity_id, ) - request_template = SyncEventsRequest( - calendar_id=calendar_id, - search=data.get(CONF_SEARCH), - start_time=dt_util.now() + SYNC_EVENT_MIN_TIME, - ) - sync = CalendarEventSyncManager( - calendar_service, - store=ScopedCalendarStore(store, unique_id or entity_name), - request_template=request_template, - ) - coordinator = CalendarUpdateCoordinator( - hass, - sync, - data[CONF_NAME], - ) + coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator + if search := data.get(CONF_SEARCH): + coordinator = CalendarQueryUpdateCoordinator( + hass, + calendar_service, + data[CONF_NAME], + calendar_id, + search, + ) + else: + request_template = SyncEventsRequest( + calendar_id=calendar_id, + start_time=dt_util.now() + SYNC_EVENT_MIN_TIME, + ) + sync = CalendarEventSyncManager( + calendar_service, + store=ScopedCalendarStore(store, unique_id or entity_name), + request_template=request_template, + ) + coordinator = CalendarSyncUpdateCoordinator( + hass, + sync, + data[CONF_NAME], + ) entities.append( GoogleCalendarEntity( coordinator, @@ -242,8 +252,8 @@ async def async_setup_entry( ) -class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]): - """Coordinator for calendar RPC calls.""" +class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): + """Coordinator for calendar RPC calls that use an efficient sync.""" def __init__( self, @@ -251,7 +261,7 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]): sync: CalendarEventSyncManager, name: str, ) -> None: - """Create the Calendar event device.""" + """Create the CalendarSyncUpdateCoordinator.""" super().__init__( hass, _LOGGER, @@ -271,6 +281,87 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]): dt_util.DEFAULT_TIME_ZONE ) + async def async_get_events( + self, start_date: datetime, end_date: datetime + ) -> Iterable[Event]: + """Get all events in a specific time frame.""" + if not self.data: + raise HomeAssistantError( + "Unable to get events: Sync from server has not completed" + ) + return self.data.overlapping( + dt_util.as_local(start_date), + dt_util.as_local(end_date), + ) + + @property + def upcoming(self) -> Iterable[Event] | None: + """Return upcoming events if any.""" + if self.data: + return self.data.active_after(dt_util.now()) + return None + + +class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]): + """Coordinator for calendar RPC calls. + + This sends a polling RPC, not using sync, as a workaround + for limitations in the calendar API for supporting search. + """ + + def __init__( + self, + hass: HomeAssistant, + calendar_service: GoogleCalendarService, + name: str, + calendar_id: str, + search: str | None, + ) -> None: + """Create the CalendarQueryUpdateCoordinator.""" + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + self.calendar_service = calendar_service + self.calendar_id = calendar_id + self._search = search + + async def async_get_events( + self, start_date: datetime, end_date: datetime + ) -> Iterable[Event]: + """Get all events in a specific time frame.""" + request = ListEventsRequest( + calendar_id=self.calendar_id, + start_time=start_date, + end_time=end_date, + search=self._search, + ) + result_items = [] + try: + result = await self.calendar_service.async_list_events(request) + async for result_page in result: + result_items.extend(result_page.items) + except ApiException as err: + self.async_set_update_error(err) + raise HomeAssistantError(str(err)) from err + return result_items + + async def _async_update_data(self) -> list[Event]: + """Fetch data from API endpoint.""" + request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) + try: + result = await self.calendar_service.async_list_events(request) + except ApiException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + return result.items + + @property + def upcoming(self) -> Iterable[Event] | None: + """Return the next upcoming event if any.""" + return self.data + class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): """A calendar event entity.""" @@ -279,7 +370,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): def __init__( self, - coordinator: CalendarUpdateCoordinator, + coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator, calendar_id: str, data: dict[str, Any], entity_id: str, @@ -352,14 +443,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - if not (timeline := self.coordinator.data): - raise HomeAssistantError( - "Unable to get events: Sync from server has not completed" - ) - result_items = timeline.overlapping( - dt_util.as_local(start_date), - dt_util.as_local(end_date), - ) + result_items = await self.coordinator.async_get_events(start_date, end_date) return [ _get_calendar_event(event) for event in filter(self._event_filter, result_items) @@ -367,14 +451,12 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): def _apply_coordinator_update(self) -> None: """Copy state from the coordinator to this entity.""" - if (timeline := self.coordinator.data) and ( - api_event := next( - filter( - self._event_filter, - timeline.active_after(dt_util.now()), - ), - None, - ) + if api_event := next( + filter( + self._event_filter, + self.coordinator.upcoming or [], + ), + None, ): self._event = _get_calendar_event(api_event) (self._event.summary, self._offset_value) = extract_offset( From 42c09d881165cfdfa112dabd3eadd3055d380df4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Nov 2022 12:44:44 +0100 Subject: [PATCH 46/50] Bumped version to 2022.11.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ef94db7e2b6..f3b51bd5d3a 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 = 11 -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) diff --git a/pyproject.toml b/pyproject.toml index e616441fadd..5058c46641f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.1" +version = "2022.11.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 345d356e9a63d6b19b74f8ed1f9f545a7f1514e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Nov 2022 13:35:12 +0100 Subject: [PATCH 47/50] Fix rest import (#81784) --- tests/components/rest/test_init.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index b6861a098b1..6f35301b1c7 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -15,6 +15,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow From 59ec8291063a89010b02b5e2ad3238f0b6b825c8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Nov 2022 15:00:30 +0100 Subject: [PATCH 48/50] Update frontend to 20221108.0 (#81787) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f4f46a1f89b..ec7001006b1 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221102.1"], + "requirements": ["home-assistant-frontend==20221108.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ebf4eff0d2b..9db814d2dea 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221102.1 +home-assistant-frontend==20221108.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index a9d1c292018..5d84cebc108 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -871,7 +871,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.1 +home-assistant-frontend==20221108.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d3fa0e0698..152bf8ee1db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -651,7 +651,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.1 +home-assistant-frontend==20221108.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 7ab20290710a267a55fa0fe1c98a8f8c4847b1b8 Mon Sep 17 00:00:00 2001 From: ztamas83 <71548739+ztamas83@users.noreply.github.com> Date: Tue, 8 Nov 2022 16:09:58 +0100 Subject: [PATCH 49/50] Retry tibber setup (#81785) * Handle integration setup retries * Fix black error * Update to falsy check Co-authored-by: Martin Hjelmare * Remove duplicated log * Update exception message Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/tibber/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 35507986f90..4d9c0560682 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -53,6 +53,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await tibber_connection.update_info() + if not tibber_connection.name: + raise ConfigEntryNotReady("Could not fetch Tibber data.") + except asyncio.TimeoutError as err: raise ConfigEntryNotReady from err except aiohttp.ClientError as err: From d88b2bf19c1fb465fe541cd3c3e000e68a99ac56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 09:20:03 -0600 Subject: [PATCH 50/50] Fix off by one in HomeKit iid allocator (#81793) --- homeassistant/components/homekit/iidmanager.py | 8 ++++---- tests/components/homekit/test_diagnostics.py | 2 -- tests/components/homekit/test_iidmanager.py | 6 ++++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/iidmanager.py b/homeassistant/components/homekit/iidmanager.py index 3805748225a..8bac268800b 100644 --- a/homeassistant/components/homekit/iidmanager.py +++ b/homeassistant/components/homekit/iidmanager.py @@ -109,12 +109,12 @@ class AccessoryIIDStorage: # AID must be a string since JSON keys cannot be int aid_str = str(aid) accessory_allocation = self.allocations.setdefault(aid_str, {}) - accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, []) + accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1]) if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None: - allocated_iid = 1 - elif allocation_key in accessory_allocation: + return 1 + if allocation_key in accessory_allocation: return accessory_allocation[allocation_key] - elif accessory_allocated_iids: + if accessory_allocated_iids: allocated_iid = accessory_allocated_iids[-1] + 1 else: allocated_iid = 2 diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index be98c3bacdd..d114c462e2f 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -51,7 +51,6 @@ async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zer "3E__23_": 5, "3E__30_": 6, "3E__52_": 7, - "3E___": 1, "A2__37_": 9, "A2___": 8, } @@ -278,7 +277,6 @@ async def test_config_entry_accessory( "3E__23_": 5, "3E__30_": 6, "3E__52_": 7, - "3E___": 1, "43__25_": 11, "43___": 10, "A2__37_": 9, diff --git a/tests/components/homekit/test_iidmanager.py b/tests/components/homekit/test_iidmanager.py index 3e4a19c9045..c16cbf01d7d 100644 --- a/tests/components/homekit/test_iidmanager.py +++ b/tests/components/homekit/test_iidmanager.py @@ -152,23 +152,29 @@ async def test_iid_generation_and_restore_v2(hass, iid_storage, hass_storage): 1, "000000AA-0000-1000-8000-0026BB765291", None, None, None ) assert not_accessory_info_service_iid == 2 + assert iid_storage.allocated_iids == {"1": [1, 2]} not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None ) assert not_accessory_info_service_iid_2 == 3 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None ) assert not_accessory_info_service_iid_2 == 3 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} accessory_info_service_iid = iid_storage.get_or_allocate_iid( 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None ) assert accessory_info_service_iid == 1 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} accessory_info_service_iid = iid_storage.get_or_allocate_iid( 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None ) assert accessory_info_service_iid == 1 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} accessory_info_service_iid = iid_storage.get_or_allocate_iid( 2, "0000003E-0000-1000-8000-0026BB765291", None, None, None ) assert accessory_info_service_iid == 1 + assert iid_storage.allocated_iids == {"1": [1, 2, 3], "2": [1]}