From f9ebbb936a496b1121d7fda874b117271adae863 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 15 Nov 2022 18:30:56 +0100 Subject: [PATCH 01/41] Fix UniFi block client switches on 2022.11.2 (#81884) fixes undefined --- homeassistant/components/unifi/switch.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 65d0041187e..43a08b58eac 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -367,8 +367,6 @@ class UnifiBlockClientSwitch(SwitchEntity): self.hass.async_create_task(self.remove_item({self._obj_id})) return - client = self.controller.api.clients[self._obj_id] - self._attr_is_on = not client.blocked self._attr_available = self.controller.available self.async_write_ha_state() From 18842ef57108a066f83d0909dd06022fe536ddd3 Mon Sep 17 00:00:00 2001 From: Yukon Vinecki Date: Wed, 16 Nov 2022 04:14:14 -0800 Subject: [PATCH 02/41] Fix Z-Wave JS cover stop support (#78723) Co-authored-by: Franck Nijhof --- homeassistant/components/zwave_js/cover.py | 20 ++------- tests/components/zwave_js/test_cover.py | 48 ++-------------------- 2 files changed, 8 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index b3f3aeaf1c0..43b51048de4 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -7,9 +7,6 @@ from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_STATE_PROPERTY, TARGET_VALUE_PROPERTY from zwave_js_server.const.command_class.barrier_operator import BarrierState from zwave_js_server.const.command_class.multilevel_switch import ( - COVER_CLOSE_PROPERTY, - COVER_DOWN_PROPERTY, - COVER_OFF_PROPERTY, COVER_ON_PROPERTY, COVER_OPEN_PROPERTY, COVER_UP_PROPERTY, @@ -156,23 +153,14 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" - open_value = ( + cover_property = ( self.get_zwave_value(COVER_OPEN_PROPERTY) or self.get_zwave_value(COVER_UP_PROPERTY) or self.get_zwave_value(COVER_ON_PROPERTY) ) - if open_value: - # Stop the cover if it's opening - await self.info.node.async_set_value(open_value, False) - - close_value = ( - self.get_zwave_value(COVER_CLOSE_PROPERTY) - or self.get_zwave_value(COVER_DOWN_PROPERTY) - or self.get_zwave_value(COVER_OFF_PROPERTY) - ) - if close_value: - # Stop the cover if it's closing - await self.info.node.async_set_value(close_value, False) + if cover_property: + # Stop the cover, will stop regardless of the actual direction of travel. + await self.info.node.async_set_value(cover_property, False) class ZWaveTiltCover(ZWaveCover): diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index f26b0d29069..0ca2e36d853 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -116,7 +116,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 + assert len(client.async_send_command.call_args_list) == 1 open_args = client.async_send_command.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 @@ -127,16 +127,6 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] - assert close_args["command"] == "node.set_value" - assert close_args["nodeId"] == 6 - assert close_args["valueId"] == { - "commandClass": 38, - "endpoint": 0, - "property": "Close", - } - assert not close_args["value"] - # Test position update from value updated event event = Event( type="value updated", @@ -189,7 +179,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 + assert len(client.async_send_command.call_args_list) == 1 open_args = client.async_send_command.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 @@ -200,16 +190,6 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] - assert close_args["command"] == "node.set_value" - assert close_args["nodeId"] == 6 - assert close_args["valueId"] == { - "commandClass": 38, - "endpoint": 0, - "property": "Close", - } - assert not close_args["value"] - client.async_send_command.reset_mock() event = Event( @@ -329,7 +309,7 @@ async def test_aeotec_nano_shutter_cover( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 + assert len(client.async_send_command.call_args_list) == 1 open_args = client.async_send_command.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 3 @@ -340,16 +320,6 @@ async def test_aeotec_nano_shutter_cover( } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] - assert close_args["command"] == "node.set_value" - assert close_args["nodeId"] == 3 - assert close_args["valueId"] == { - "commandClass": 38, - "endpoint": 0, - "property": "Off", - } - assert not close_args["value"] - # Test position update from value updated event event = Event( type="value updated", @@ -403,7 +373,7 @@ async def test_aeotec_nano_shutter_cover( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 + assert len(client.async_send_command.call_args_list) == 1 open_args = client.async_send_command.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 3 @@ -414,16 +384,6 @@ async def test_aeotec_nano_shutter_cover( } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] - assert close_args["command"] == "node.set_value" - assert close_args["nodeId"] == 3 - assert close_args["valueId"] == { - "commandClass": 38, - "endpoint": 0, - "property": "Off", - } - assert not close_args["value"] - async def test_blind_cover(hass, client, iblinds_v2, integration): """Test a blind cover entity.""" From d94e969dc11a4ee9d7aff11ff630f5968a2d0918 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Nov 2022 11:41:55 -0600 Subject: [PATCH 03/41] Fix instability with HomeKit trigger accessories (#80703) fixes https://github.com/home-assistant/core/issues/78774 fixes https://github.com/home-assistant/core/issues/81685 --- homeassistant/components/homekit/__init__.py | 61 ++-- .../components/homekit/accessories.py | 2 +- .../components/homekit/diagnostics.py | 11 +- .../components/homekit/type_triggers.py | 22 +- tests/components/homekit/test_diagnostics.py | 322 +++++++++++++++++- tests/components/homekit/test_init.py | 64 +++- 6 files changed, 450 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ca73c7dc242..1129e8c3f66 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -23,6 +23,9 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, ) from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.device_automation.trigger import ( + async_validate_trigger_config, +) from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN from homeassistant.components.network import MDNS_TARGET_IP @@ -906,29 +909,47 @@ class HomeKit: self.bridge = HomeBridge(self.hass, self.driver, self._name) for state in entity_states: self.add_bridge_accessory(state) - dev_reg = device_registry.async_get(self.hass) if self._devices: - valid_device_ids = [] - for device_id in self._devices: - if not dev_reg.async_get(device_id): - _LOGGER.warning( - "HomeKit %s cannot add device %s because it is missing from the device registry", - self._name, - device_id, - ) - else: - valid_device_ids.append(device_id) - for device_id, device_triggers in ( - await device_automation.async_get_device_automations( - self.hass, - device_automation.DeviceAutomationType.TRIGGER, - valid_device_ids, - ) - ).items(): - if device := dev_reg.async_get(device_id): - self.add_bridge_triggers_accessory(device, device_triggers) + await self._async_add_trigger_accessories() return self.bridge + async def _async_add_trigger_accessories(self) -> None: + """Add devices with triggers to the bridge.""" + dev_reg = device_registry.async_get(self.hass) + valid_device_ids = [] + for device_id in self._devices: + if not dev_reg.async_get(device_id): + _LOGGER.warning( + "HomeKit %s cannot add device %s because it is missing from the device registry", + self._name, + device_id, + ) + else: + valid_device_ids.append(device_id) + for device_id, device_triggers in ( + await device_automation.async_get_device_automations( + self.hass, + device_automation.DeviceAutomationType.TRIGGER, + valid_device_ids, + ) + ).items(): + device = dev_reg.async_get(device_id) + assert device is not None + valid_device_triggers: list[dict[str, Any]] = [] + for trigger in device_triggers: + try: + await async_validate_trigger_config(self.hass, trigger) + except vol.Invalid as ex: + _LOGGER.debug( + "%s: cannot add unsupported trigger %s because it requires additional inputs which are not supported by HomeKit: %s", + self._name, + trigger, + ex, + ) + continue + valid_device_triggers.append(trigger) + self.add_bridge_triggers_accessory(device, valid_device_triggers) + async def _async_create_accessories(self) -> bool: """Create the accessories.""" assert self.driver is not None diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 44b73b41ded..aca53a50105 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -653,7 +653,7 @@ class HomeIIDManager(IIDManager): # type: ignore[misc] """Get IID for object.""" aid = obj.broker.aid if isinstance(obj, Characteristic): - service = obj.service + service: Service = obj.service iid = self._iid_storage.get_or_allocate_iid( aid, service.type_id, service.unique_id, obj.type_id, obj.unique_id ) diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py index 1d0bfb92fcc..f27171e6eae 100644 --- a/homeassistant/components/homekit/diagnostics.py +++ b/homeassistant/components/homekit/diagnostics.py @@ -67,13 +67,16 @@ def _get_accessory_diagnostics( hass: HomeAssistant, accessory: HomeAccessory ) -> dict[str, Any]: """Return diagnostics for an accessory.""" - return { + entity_state = None + if accessory.entity_id: + entity_state = hass.states.get(accessory.entity_id) + data = { "aid": accessory.aid, "config": accessory.config, "category": accessory.category, "name": accessory.display_name, "entity_id": accessory.entity_id, - "entity_state": async_redact_data( - hass.states.get(accessory.entity_id), TO_REDACT - ), } + if entity_state: + data["entity_state"] = async_redact_data(entity_state, TO_REDACT) + return data diff --git a/homeassistant/components/homekit/type_triggers.py b/homeassistant/components/homekit/type_triggers.py index b9b2ad6ce8f..b239d67877c 100644 --- a/homeassistant/components/homekit/type_triggers.py +++ b/homeassistant/components/homekit/type_triggers.py @@ -7,9 +7,11 @@ from typing import Any from pyhap.const import CATEGORY_SENSOR from homeassistant.core import CALLBACK_TYPE, Context +from homeassistant.helpers import entity_registry from homeassistant.helpers.trigger import async_initialize_triggers from .accessories import TYPES, HomeAccessory +from .aidmanager import get_system_unique_id from .const import ( CHAR_NAME, CHAR_PROGRAMMABLE_SWITCH_EVENT, @@ -18,6 +20,7 @@ from .const import ( SERV_SERVICE_LABEL, SERV_STATELESS_PROGRAMMABLE_SWITCH, ) +from .util import cleanup_name_for_homekit _LOGGER = logging.getLogger(__name__) @@ -39,13 +42,22 @@ class DeviceTriggerAccessory(HomeAccessory): self._remove_triggers: CALLBACK_TYPE | None = None self.triggers = [] assert device_triggers is not None + ent_reg = entity_registry.async_get(self.hass) for idx, trigger in enumerate(device_triggers): - type_ = trigger["type"] - subtype = trigger.get("subtype") + type_: str = trigger["type"] + subtype: str | None = trigger.get("subtype") unique_id = f'{type_}-{subtype or ""}' - trigger_name = ( - f"{type_.title()} {subtype.title()}" if subtype else type_.title() - ) + if (entity_id := trigger.get("entity_id")) and ( + entry := ent_reg.async_get(entity_id) + ): + unique_id += f"-entity_unique_id:{get_system_unique_id(entry)}" + trigger_name_parts = [] + if entity_id and (state := self.hass.states.get(entity_id)): + trigger_name_parts.append(state.name) + trigger_name_parts.append(type_.replace("_", " ").title()) + if subtype: + trigger_name_parts.append(subtype.replace("_", " ").title()) + trigger_name = cleanup_name_for_homekit(" ".join(trigger_name_parts)) serv_stateless_switch = self.add_preload_service( SERV_STATELESS_PROGRAMMABLE_SWITCH, [CHAR_NAME, CHAR_SERVICE_LABEL_INDEX], diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index d114c462e2f..30fe5f2d8fc 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -1,12 +1,14 @@ """Test homekit diagnostics.""" -from unittest.mock import ANY, patch +from unittest.mock import ANY, MagicMock, patch from homeassistant.components.homekit.const import ( + CONF_DEVICES, CONF_HOMEKIT_MODE, DOMAIN, HOMEKIT_MODE_ACCESSORY, ) from homeassistant.const import CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STARTED +from homeassistant.setup import async_setup_component from .util import async_init_integration @@ -290,3 +292,321 @@ async def test_config_entry_accessory( ), patch("homeassistant.components.homekit.async_port_is_available"): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_config_entry_with_trigger_accessory( + hass, + hass_client, + hk_driver, + mock_async_zeroconf, + events, + demo_cleanup, + device_reg, + entity_reg, +): + """Test generating diagnostics for a bridge config entry with a trigger accessory.""" + assert await async_setup_component(hass, "demo", {"demo": {}}) + hk_driver.publish = MagicMock() + + demo_config_entry = MockConfigEntry(domain="domain") + demo_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, "demo", {"demo": {}}) + await hass.async_block_till_done() + + entry = entity_reg.async_get("light.ceiling_lights") + assert entry is not None + device_id = entry.device_id + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: "mock_name", + CONF_PORT: 12345, + CONF_DEVICES: [device_id], + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["light.none"], + }, + }, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + diag.pop("iid_storage") + diag.pop("bridge") + assert diag == { + "accessories": [ + { + "aid": 1, + "services": [ + { + "characteristics": [ + {"format": "bool", "iid": 2, "perms": ["pw"], "type": "14"}, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "20", + "value": "Home Assistant", + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "21", + "value": "Bridge", + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "23", + "value": "mock_name", + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "30", + "value": "homekit.bridge", + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "52", + "value": "2022.12.0", + }, + ], + "iid": 1, + "type": "3E", + }, + { + "characteristics": [ + { + "format": "string", + "iid": 9, + "perms": ["pr", "ev"], + "type": "37", + "value": "01.01.00", + } + ], + "iid": 8, + "type": "A2", + }, + ], + }, + { + "aid": ANY, + "services": [ + { + "characteristics": [ + {"format": "bool", "iid": 2, "perms": ["pw"], "type": "14"}, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "20", + "value": "Demo", + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "21", + "value": "Home Assistant", + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "23", + "value": "Ceiling Lights", + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "30", + "value": ANY, + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "52", + "value": ANY, + }, + ], + "iid": 1, + "type": "3E", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 9, + "perms": ["pr", "ev"], + "type": "73", + "valid-values": [0], + "value": None, + }, + { + "format": "string", + "iid": 10, + "perms": ["pr"], + "type": "23", + "value": "Ceiling Lights " "Changed States", + }, + { + "format": "uint8", + "iid": 11, + "maxValue": 255, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "CB", + "value": 1, + }, + ], + "iid": 8, + "linked": [12], + "type": "89", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 13, + "perms": ["pr"], + "type": "CD", + "valid-values": [0, 1], + "value": 1, + } + ], + "iid": 12, + "type": "CC", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 15, + "perms": ["pr", "ev"], + "type": "73", + "valid-values": [0], + "value": None, + }, + { + "format": "string", + "iid": 16, + "perms": ["pr"], + "type": "23", + "value": "Ceiling Lights " "Turned Off", + }, + { + "format": "uint8", + "iid": 17, + "maxValue": 255, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "CB", + "value": 2, + }, + ], + "iid": 14, + "linked": [18], + "type": "89", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 19, + "perms": ["pr"], + "type": "CD", + "valid-values": [0, 1], + "value": 1, + } + ], + "iid": 18, + "type": "CC", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 21, + "perms": ["pr", "ev"], + "type": "73", + "valid-values": [0], + "value": None, + }, + { + "format": "string", + "iid": 22, + "perms": ["pr"], + "type": "23", + "value": "Ceiling Lights " "Turned On", + }, + { + "format": "uint8", + "iid": 23, + "maxValue": 255, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "CB", + "value": 3, + }, + ], + "iid": 20, + "linked": [24], + "type": "89", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 25, + "perms": ["pr"], + "type": "CD", + "valid-values": [0, 1], + "value": 1, + } + ], + "iid": 24, + "type": "CC", + }, + ], + }, + ], + "client_properties": {}, + "config-entry": { + "data": {"name": "mock_name", "port": 12345}, + "options": { + "devices": [device_id], + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["light.none"], + }, + }, + "title": "Mock Title", + "version": 1, + }, + "config_version": 2, + "pairing_id": ANY, + "status": 1, + } + with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( + "homeassistant.components.homekit.HomeKit.async_stop" + ), patch("homeassistant.components.homekit.async_port_is_available"): + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 17933616fc4..f91855b31b5 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -7,9 +7,17 @@ from homeassistant.components.homekit.const import ( DOMAIN as DOMAIN_HOMEKIT, EVENT_HOMEKIT_CHANGED, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SERVICE, + EVENT_HOMEASSISTANT_STARTED, +) from homeassistant.setup import async_setup_component +from .util import PATH_HOMEKIT + +from tests.common import MockConfigEntry from tests.components.logbook.common import MockRow, mock_humanify @@ -52,3 +60,57 @@ async def test_humanify_homekit_changed_event(hass, hk_driver, mock_get_source_i assert event2["domain"] == DOMAIN_HOMEKIT assert event2["message"] == "send command set_cover_position to 75 for Window" assert event2["entity_id"] == "cover.window" + + +async def test_bridge_with_triggers( + hass, hk_driver, mock_async_zeroconf, entity_reg, caplog +): + """Test we can setup a bridge with triggers and we ignore numeric states. + + Since numeric states are not supported by HomeKit as they require + an above or below additional configuration which we have no way + to input, we ignore them. + """ + assert await async_setup_component(hass, "demo", {"demo": {}}) + await hass.async_block_till_done() + + entry = entity_reg.async_get("cover.living_room_window") + assert entry is not None + device_id = entry.device_id + + entry = MockConfigEntry( + domain=DOMAIN_HOMEKIT, + source=SOURCE_ZEROCONF, + data={ + "name": "HASS Bridge", + "port": 12345, + }, + options={ + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["cover.living_room_window"], + }, + "exclude_accessory_mode": True, + "mode": "bridge", + "devices": [device_id], + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ), patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert ( + "requires additional inputs which are not supported by HomeKit" in caplog.text + ) From 7d20bb05326de46eabc6f78a31f1761350138117 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sat, 12 Nov 2022 10:43:11 +0100 Subject: [PATCH 04/41] Fix accelerator sensor in fibaro integration (#81237) * Fix accelerator sensor in fibaro integration * Implement suggestions from code review * Implement suggestions from code review * Changes as suggested in code review * Adjust as suggested in code review --- homeassistant/components/fibaro/__init__.py | 1 + .../components/fibaro/binary_sensor.py | 56 ++++++++++++++++--- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 3661721810b..19f36742740 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -84,6 +84,7 @@ FIBARO_TYPEMAP = { "com.fibaro.thermostatDanfoss": Platform.CLIMATE, "com.fibaro.doorLock": Platform.LOCK, "com.fibaro.binarySensor": Platform.BINARY_SENSOR, + "com.fibaro.accelerometer": Platform.BINARY_SENSOR, } DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 359869efc25..f9baa33c41f 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Fibaro binary sensors.""" from __future__ import annotations +from collections.abc import Mapping +import json from typing import Any from homeassistant.components.binary_sensor import ( @@ -28,6 +30,11 @@ SENSOR_TYPES = { "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", BinarySensorDeviceClass.SMOKE], "com.fibaro.FGMS001": ["Motion", "mdi:run", BinarySensorDeviceClass.MOTION], "com.fibaro.heatDetector": ["Heat", "mdi:fire", BinarySensorDeviceClass.HEAT], + "com.fibaro.accelerometer": [ + "Moving", + "mdi:axis-arrow", + BinarySensorDeviceClass.MOVING, + ], } @@ -55,15 +62,50 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): """Initialize the binary_sensor.""" super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - stype = None + self._own_extra_state_attributes: Mapping[str, Any] = {} + self._fibaro_sensor_type = None if fibaro_device.type in SENSOR_TYPES: - stype = fibaro_device.type + self._fibaro_sensor_type = fibaro_device.type elif fibaro_device.baseType in SENSOR_TYPES: - stype = fibaro_device.baseType - if stype: - self._attr_device_class = SENSOR_TYPES[stype][2] - self._attr_icon = SENSOR_TYPES[stype][1] + self._fibaro_sensor_type = fibaro_device.baseType + if self._fibaro_sensor_type: + self._attr_device_class = SENSOR_TYPES[self._fibaro_sensor_type][2] + self._attr_icon = SENSOR_TYPES[self._fibaro_sensor_type][1] + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return the extra state attributes of the device.""" + return super().extra_state_attributes | self._own_extra_state_attributes def update(self) -> None: """Get the latest data and update the state.""" - self._attr_is_on = self.current_binary_state + if self._fibaro_sensor_type == "com.fibaro.accelerometer": + # Accelerator sensors have values for the three axis x, y and z + moving_values = self._get_moving_values() + self._attr_is_on = self._is_moving(moving_values) + self._own_extra_state_attributes = self._get_xyz_moving(moving_values) + else: + self._attr_is_on = self.current_binary_state + + def _get_xyz_moving(self, moving_values: Mapping[str, Any]) -> Mapping[str, Any]: + """Return x y z values of the accelerator sensor value.""" + attrs = {} + for axis_name in ("x", "y", "z"): + attrs[axis_name] = float(moving_values[axis_name]) + return attrs + + def _is_moving(self, moving_values: Mapping[str, Any]) -> bool: + """Return that a moving is detected when one axis reports a value.""" + for axis_name in ("x", "y", "z"): + if float(moving_values[axis_name]) != 0: + return True + return False + + def _get_moving_values(self) -> Mapping[str, Any]: + """Get the moving values of the accelerator sensor in a dict.""" + value = self.fibaro_device.properties.value + if isinstance(value, str): + # HC2 returns dict as str + return json.loads(value) + # HC3 returns a real dict + return value From 3f666396c9933bc21ffdcd90dccecd98a0e7d932 Mon Sep 17 00:00:00 2001 From: chpego <38792705+chpego@users.noreply.github.com> Date: Tue, 8 Nov 2022 13:39:53 +0000 Subject: [PATCH 05/41] Fix Fully Kiosk start application service field (#81738) Fix attributes services to start_application --- homeassistant/components/fully_kiosk/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fully_kiosk/services.yaml b/homeassistant/components/fully_kiosk/services.yaml index b8ea6b371d7..88178e35809 100644 --- a/homeassistant/components/fully_kiosk/services.yaml +++ b/homeassistant/components/fully_kiosk/services.yaml @@ -20,7 +20,7 @@ start_application: device: integration: fully_kiosk fields: - url: + application: name: Application description: Package name of the application to start. example: "de.ozerov.fully" From 2eacbef0610ea930d5b96450a7ea4b9675d5dc66 Mon Sep 17 00:00:00 2001 From: Oliver Dippel Date: Sun, 13 Nov 2022 21:17:59 +0100 Subject: [PATCH 06/41] Fix ibeacon source attribute not being updated (#81740) fixes undefined --- .../components/ibeacon/coordinator.py | 6 +- tests/components/ibeacon/test_coordinator.py | 69 ++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 33b33c56ed0..ed62649de2d 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -396,7 +396,11 @@ class IBeaconCoordinator: ) continue - if service_info.rssi != ibeacon_advertisement.rssi: + if ( + service_info.rssi != ibeacon_advertisement.rssi + or service_info.source != ibeacon_advertisement.source + ): + ibeacon_advertisement.source = service_info.source ibeacon_advertisement.update_rssi(service_info.rssi) async_dispatcher_send( self.hass, diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py index 25ce7154a37..6acbf5569f8 100644 --- a/tests/components/ibeacon/test_coordinator.py +++ b/tests/components/ibeacon/test_coordinator.py @@ -3,16 +3,19 @@ from dataclasses import replace from datetime import timedelta +import time +from bleak.backends.scanner import BLEDevice import pytest -from homeassistant.components.ibeacon.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.components.ibeacon.const import ATTR_SOURCE, DOMAIN, UPDATE_INTERVAL from homeassistant.const import STATE_HOME from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.util import dt as dt_util from . import ( BLUECHARM_BEACON_SERVICE_INFO, + BLUECHARM_BEACON_SERVICE_INFO_2, BLUECHARM_BEACON_SERVICE_INFO_DBUS, TESLA_TRANSIENT, TESLA_TRANSIENT_BLE_DEVICE, @@ -20,6 +23,8 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.bluetooth import ( + generate_advertisement_data, + inject_advertisement_with_time_and_source_connectable, inject_bluetooth_service_info, patch_all_discovered_devices, ) @@ -252,3 +257,65 @@ async def test_ignore_transient_devices_unless_we_see_them_a_few_times(hass): await hass.async_block_till_done() assert hass.states.get("device_tracker.s6da7c9389bd5452cc_cccc").state == STATE_HOME + + +async def test_changing_source_attribute(hass): + """Test update of the source attribute.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + now = time.monotonic() + info = BLUECHARM_BEACON_SERVICE_INFO_2 + device = BLEDevice( + address=info.address, + name=info.name, + details={}, + ) + advertisement_data = generate_advertisement_data( + local_name=info.name, + manufacturer_data=info.manufacturer_data, + service_data=info.service_data, + service_uuids=info.service_uuids, + rssi=info.rssi, + ) + + inject_advertisement_with_time_and_source_connectable( + hass, + device, + advertisement_data, + now, + "local", + True, + ) + await hass.async_block_till_done() + + attributes = hass.states.get( + "sensor.bluecharm_177999_8105_estimated_distance" + ).attributes + assert attributes[ATTR_SOURCE] == "local" + + inject_advertisement_with_time_and_source_connectable( + hass, + device, + advertisement_data, + now, + "proxy", + True, + ) + await hass.async_block_till_done() + with patch_all_discovered_devices([BLUECHARM_BEACON_SERVICE_INFO_2]): + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL.total_seconds() * 2), + ) + await hass.async_block_till_done() + + attributes = hass.states.get( + "sensor.bluecharm_177999_8105_estimated_distance" + ).attributes + assert attributes[ATTR_SOURCE] == "proxy" From 252941ae263286df6a9b738d39c074f092cb27b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 9 Nov 2022 16:51:33 +0200 Subject: [PATCH 07/41] Upgrade huawei-lte-api to 1.6.7, fixes empty username issues (#81751) Recentish versions of huawei-lte-api behave differently with regards to empty/default username compared to the older versions this integration was originally written against. 1.6.5+ changes the behavior so that our existing implementation works as expected when no username is supplied for the config entry. https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.4 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.5 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.6 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.7 --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index c658fff1b0f..2c777aa4339 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "huawei-lte-api==1.6.3", + "huawei-lte-api==1.6.7", "stringcase==1.2.0", "url-normalize==1.4.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 5d84cebc108..23accfd4b89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -889,7 +889,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.3 +huawei-lte-api==1.6.7 # homeassistant.components.hydrawise hydrawiser==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 152bf8ee1db..67f2709e1c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -666,7 +666,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.3 +huawei-lte-api==1.6.7 # homeassistant.components.hyperion hyperion-py==0.7.5 From 04fda5638c7ee445fe86e28e7a1b8c411d71f7e8 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 9 Nov 2022 08:44:30 -0600 Subject: [PATCH 08/41] Change life360 timeouts & retries (#81799) Change from single timeout of 10 to socket timeout of 15, total timeout of 60, and retry up to 3 times. Bump life360 package to 5.3.0. --- homeassistant/components/life360/const.py | 5 ++++- homeassistant/components/life360/coordinator.py | 2 ++ homeassistant/components/life360/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/life360/const.py b/homeassistant/components/life360/const.py index d148a06c634..333ce14fbf6 100644 --- a/homeassistant/components/life360/const.py +++ b/homeassistant/components/life360/const.py @@ -3,11 +3,14 @@ from datetime import timedelta import logging +from aiohttp import ClientTimeout + DOMAIN = "life360" LOGGER = logging.getLogger(__package__) ATTRIBUTION = "Data provided by life360.com" -COMM_TIMEOUT = 10 +COMM_MAX_RETRIES = 3 +COMM_TIMEOUT = ClientTimeout(sock_connect=15, total=60) SPEED_FACTOR_MPH = 2.25 SPEED_DIGITS = 1 UPDATE_INTERVAL = timedelta(seconds=10) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index 0b9641bfcae..b7121cc7fdb 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -26,6 +26,7 @@ from homeassistant.util.unit_conversion import DistanceConverter from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( + COMM_MAX_RETRIES, COMM_TIMEOUT, CONF_AUTHORIZATION, DOMAIN, @@ -106,6 +107,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): self._api = Life360( session=async_get_clientsession(hass), timeout=COMM_TIMEOUT, + max_retries=COMM_MAX_RETRIES, authorization=entry.data[CONF_AUTHORIZATION], ) self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 8f0c44f342b..eb3290e41e1 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/life360", "codeowners": ["@pnbruckner"], - "requirements": ["life360==5.1.1"], + "requirements": ["life360==5.3.0"], "iot_class": "cloud_polling", "loggers": ["life360"] } diff --git a/requirements_all.txt b/requirements_all.txt index 23accfd4b89..79643e571d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1006,7 +1006,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.1.1 +life360==5.3.0 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67f2709e1c0..4ab4ae3bd9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -744,7 +744,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.1.1 +life360==5.3.0 # homeassistant.components.logi_circle logi_circle==0.2.3 From 3a60466e7ce19b98f6f5d37d885356519fdb2a16 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 01:09:06 -0600 Subject: [PATCH 09/41] Fix switchbot not becoming available again after unavailable (#81822) * Fix switchbot not becoming available again after unavailable If the advertisment was the same and we were previously marked as unavailable we would not mark the device as available again until the advertisment changed. For lights there is a counter but for the bots there is no counter which means the bots would show unavailable even though they were available again * naming * naming --- .../components/switchbot/coordinator.py | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index ee93c74af37..f68c1effc0c 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -61,6 +61,15 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): self.base_unique_id = base_unique_id self.model = model self._ready_event = asyncio.Event() + self._was_unavailable = True + + @callback + def _async_handle_unavailable( + self, service_info: bluetooth.BluetoothServiceInfoBleak + ) -> None: + """Handle the device going unavailable.""" + super()._async_handle_unavailable(service_info) + self._was_unavailable = True @callback def _async_handle_bluetooth_event( @@ -70,16 +79,20 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): ) -> None: """Handle a Bluetooth event.""" self.ble_device = service_info.device - if adv := switchbot.parse_advertisement_data( - service_info.device, service_info.advertisement + if not ( + adv := switchbot.parse_advertisement_data( + service_info.device, service_info.advertisement + ) ): - if "modelName" in adv.data: - self._ready_event.set() - _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) - if not self.device.advertisement_changed(adv): - return - self.data = flatten_sensors_data(adv.data) - self.device.update_from_advertisement(adv) + return + if "modelName" in adv.data: + self._ready_event.set() + _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) + if not self.device.advertisement_changed(adv) and not self._was_unavailable: + return + self._was_unavailable = False + self.data = flatten_sensors_data(adv.data) + self.device.update_from_advertisement(adv) super()._async_handle_bluetooth_event(service_info, change) async def async_wait_ready(self) -> bool: From 248ed3660fa7d4b236d458f1d82325db9ad821d1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Nov 2022 12:46:29 +0100 Subject: [PATCH 10/41] Fix statistic_during_period for data with holes (#81847) --- .../components/recorder/statistics.py | 201 +++++++----- .../components/recorder/test_websocket_api.py | 289 +++++++++++++++--- 2 files changed, 375 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 8a744fd4daa..e361249580f 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1216,11 +1216,29 @@ def _get_max_mean_min_statistic( return result +def _first_statistic( + session: Session, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, +) -> datetime | None: + """Return the data of the oldest statistic row for a given metadata id.""" + stmt = lambda_stmt( + lambda: select(table.start) + .filter(table.metadata_id == metadata_id) + .order_by(table.start.asc()) + .limit(1) + ) + if stats := execute_stmt_lambda_element(session, stmt): + return process_timestamp(stats[0].start) # type: ignore[no-any-return] + return None + + def _get_oldest_sum_statistic( session: Session, head_start_time: datetime | None, main_start_time: datetime | None, tail_start_time: datetime | None, + oldest_stat: datetime | None, tail_only: bool, metadata_id: int, ) -> float | None: @@ -1231,10 +1249,10 @@ def _get_oldest_sum_statistic( start_time: datetime | None, table: type[Statistics | StatisticsShortTerm], metadata_id: int, - ) -> tuple[float | None, datetime | None]: + ) -> float | None: """Return the oldest non-NULL sum during the period.""" stmt = lambda_stmt( - lambda: select(table.sum, table.start) + lambda: select(table.sum) .filter(table.metadata_id == metadata_id) .filter(table.sum.is_not(None)) .order_by(table.start.asc()) @@ -1248,49 +1266,49 @@ def _get_oldest_sum_statistic( else: period = start_time.replace(minute=0, second=0, microsecond=0) prev_period = period - table.duration - stmt += lambda q: q.filter(table.start == prev_period) + stmt += lambda q: q.filter(table.start >= prev_period) stats = execute_stmt_lambda_element(session, stmt) - return ( - (stats[0].sum, process_timestamp(stats[0].start)) if stats else (None, None) - ) + return stats[0].sum if stats else None - oldest_start: datetime | None oldest_sum: float | None = None - if head_start_time is not None: - oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( - session, head_start_time, StatisticsShortTerm, metadata_id + # This function won't be called if tail_only is False and main_start_time is None + # the extra checks are added to satisfy MyPy + if not tail_only and main_start_time is not None and oldest_stat is not None: + period = main_start_time.replace(minute=0, second=0, microsecond=0) + prev_period = period - Statistics.duration + if prev_period < oldest_stat: + return 0 + + if ( + head_start_time is not None + and ( + oldest_sum := _get_oldest_sum_statistic_in_sub_period( + session, head_start_time, StatisticsShortTerm, metadata_id + ) ) - if ( - oldest_start is not None - and oldest_start < head_start_time - and oldest_sum is not None - ): - return oldest_sum + is not None + ): + return oldest_sum if not tail_only: - assert main_start_time is not None - oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( - session, main_start_time, Statistics, metadata_id - ) if ( - oldest_start is not None - and oldest_start < main_start_time - and oldest_sum is not None - ): + oldest_sum := _get_oldest_sum_statistic_in_sub_period( + session, main_start_time, Statistics, metadata_id + ) + ) is not None: return oldest_sum return 0 - if tail_start_time is not None: - oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( - session, tail_start_time, StatisticsShortTerm, metadata_id + if ( + tail_start_time is not None + and ( + oldest_sum := _get_oldest_sum_statistic_in_sub_period( + session, tail_start_time, StatisticsShortTerm, metadata_id + ) ) - if ( - oldest_start is not None - and oldest_start < tail_start_time - and oldest_sum is not None - ): - return oldest_sum + ) is not None: + return oldest_sum return 0 @@ -1373,51 +1391,79 @@ def statistic_during_period( result: dict[str, Any] = {} - # To calculate the summary, data from the statistics (hourly) and short_term_statistics - # (5 minute) tables is combined - # - The short term statistics table is used for the head and tail of the period, - # if the period it doesn't start or end on a full hour - # - The statistics table is used for the remainder of the time - now = dt_util.utcnow() - if end_time is not None and end_time > now: - end_time = now - - tail_only = ( - start_time is not None - and end_time is not None - and end_time - start_time < timedelta(hours=1) - ) - - # Calculate the head period - head_start_time: datetime | None = None - head_end_time: datetime | None = None - if not tail_only and start_time is not None and start_time.minute: - head_start_time = start_time - head_end_time = start_time.replace( - minute=0, second=0, microsecond=0 - ) + timedelta(hours=1) - - # Calculate the tail period - tail_start_time: datetime | None = None - tail_end_time: datetime | None = None - if end_time is None: - tail_start_time = now.replace(minute=0, second=0, microsecond=0) - elif end_time.minute: - tail_start_time = ( - start_time - if tail_only - else end_time.replace(minute=0, second=0, microsecond=0) - ) - tail_end_time = end_time - - # Calculate the main period - main_start_time: datetime | None = None - main_end_time: datetime | None = None - if not tail_only: - main_start_time = start_time if head_end_time is None else head_end_time - main_end_time = end_time if tail_start_time is None else tail_start_time - with session_scope(hass=hass) as session: + # Fetch metadata for the given statistic_id + if not ( + metadata := get_metadata_with_session(session, statistic_ids=[statistic_id]) + ): + return result + + metadata_id = metadata[statistic_id][0] + + oldest_stat = _first_statistic(session, Statistics, metadata_id) + oldest_5_min_stat = None + if not valid_statistic_id(statistic_id): + oldest_5_min_stat = _first_statistic( + session, StatisticsShortTerm, metadata_id + ) + + # To calculate the summary, data from the statistics (hourly) and + # short_term_statistics (5 minute) tables is combined + # - The short term statistics table is used for the head and tail of the period, + # if the period it doesn't start or end on a full hour + # - The statistics table is used for the remainder of the time + now = dt_util.utcnow() + if end_time is not None and end_time > now: + end_time = now + + tail_only = ( + start_time is not None + and end_time is not None + and end_time - start_time < timedelta(hours=1) + ) + + # Calculate the head period + head_start_time: datetime | None = None + head_end_time: datetime | None = None + if ( + not tail_only + and oldest_stat is not None + and oldest_5_min_stat is not None + and oldest_5_min_stat - oldest_stat < timedelta(hours=1) + and (start_time is None or start_time < oldest_5_min_stat) + ): + # To improve accuracy of averaged for statistics which were added within + # recorder's retention period. + head_start_time = oldest_5_min_stat + head_end_time = oldest_5_min_stat.replace( + minute=0, second=0, microsecond=0 + ) + timedelta(hours=1) + elif not tail_only and start_time is not None and start_time.minute: + head_start_time = start_time + head_end_time = start_time.replace( + minute=0, second=0, microsecond=0 + ) + timedelta(hours=1) + + # Calculate the tail period + tail_start_time: datetime | None = None + tail_end_time: datetime | None = None + if end_time is None: + tail_start_time = now.replace(minute=0, second=0, microsecond=0) + elif end_time.minute: + tail_start_time = ( + start_time + if tail_only + else end_time.replace(minute=0, second=0, microsecond=0) + ) + tail_end_time = end_time + + # Calculate the main period + main_start_time: datetime | None = None + main_end_time: datetime | None = None + if not tail_only: + main_start_time = start_time if head_end_time is None else head_end_time + main_end_time = end_time if tail_start_time is None else tail_start_time + # Fetch metadata for the given statistic_id metadata = get_metadata_with_session(session, statistic_ids=[statistic_id]) if not metadata: @@ -1449,6 +1495,7 @@ def statistic_during_period( head_start_time, main_start_time, tail_start_time, + oldest_stat, tail_only, metadata_id, ) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 00e9d0d35b4..423ab4bbc52 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -182,7 +182,8 @@ async def test_statistics_during_period(recorder_mock, hass, hass_ws_client): @freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) -async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): +@pytest.mark.parametrize("offset", (0, 1, 2)) +async def test_statistic_during_period(recorder_mock, hass, hass_ws_client, offset): """Test statistic_during_period.""" id = 1 @@ -197,7 +198,9 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): client = await hass_ws_client() zero = now - start = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=-3) + start = zero.replace(minute=offset * 5, second=0, microsecond=0) + timedelta( + hours=-3 + ) imported_stats_5min = [ { @@ -209,22 +212,37 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } for i in range(0, 39) ] - imported_stats = [ + imported_stats = [] + slice_end = 12 - offset + imported_stats.append( { - "start": imported_stats_5min[i * 12]["start"], - "max": max( - stat["max"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] - ), - "mean": fmean( - stat["mean"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] - ), - "min": min( - stat["min"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] - ), - "sum": imported_stats_5min[i * 12 + 11]["sum"], + "start": imported_stats_5min[0]["start"].replace(minute=0), + "max": max(stat["max"] for stat in imported_stats_5min[0:slice_end]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[0:slice_end]), + "min": min(stat["min"] for stat in imported_stats_5min[0:slice_end]), + "sum": imported_stats_5min[slice_end - 1]["sum"], } - for i in range(0, 3) - ] + ) + for i in range(0, 2): + slice_start = i * 12 + (12 - offset) + slice_end = (i + 1) * 12 + (12 - offset) + assert imported_stats_5min[slice_start]["start"].minute == 0 + imported_stats.append( + { + "start": imported_stats_5min[slice_start]["start"], + "max": max( + stat["max"] for stat in imported_stats_5min[slice_start:slice_end] + ), + "mean": fmean( + stat["mean"] for stat in imported_stats_5min[slice_start:slice_end] + ), + "min": min( + stat["min"] for stat in imported_stats_5min[slice_start:slice_end] + ), + "sum": imported_stats_5min[slice_end - 1]["sum"], + } + ) + imported_metadata = { "has_mean": False, "has_sum": True, @@ -285,8 +303,14 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should also include imported_statistics_5min[:] - start_time = "2022-10-21T04:00:00+00:00" - end_time = "2022-10-21T07:15:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T04:00:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() + end_time = ( + dt_util.parse_datetime("2022-10-21T07:15:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() await client.send_json( { "id": next_id(), @@ -308,8 +332,14 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should also include imported_statistics_5min[:] - start_time = "2022-10-20T04:00:00+00:00" - end_time = "2022-10-21T08:20:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T04:00:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() + end_time = ( + dt_util.parse_datetime("2022-10-21T08:20:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() await client.send_json( { "id": next_id(), @@ -331,7 +361,10 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should include imported_statistics_5min[26:] - start_time = "2022-10-21T06:10:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T06:10:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == start_time await client.send_json( { @@ -353,7 +386,10 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should also include imported_statistics_5min[26:] - start_time = "2022-10-21T06:09:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T06:09:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() await client.send_json( { "id": next_id(), @@ -374,7 +410,10 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should include imported_statistics_5min[:26] - end_time = "2022-10-21T06:10:00+00:00" + end_time = ( + dt_util.parse_datetime("2022-10-21T06:10:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == end_time await client.send_json( { @@ -396,9 +435,15 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should include imported_statistics_5min[26:32] (less than a full hour) - start_time = "2022-10-21T06:10:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T06:10:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == start_time - end_time = "2022-10-21T06:40:00+00:00" + end_time = ( + dt_util.parse_datetime("2022-10-21T06:40:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() assert imported_stats_5min[32]["start"].isoformat() == end_time await client.send_json( { @@ -422,7 +467,7 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): # This should include imported_statistics[2:] + imported_statistics_5min[36:] start_time = "2022-10-21T06:00:00+00:00" - assert imported_stats_5min[24]["start"].isoformat() == start_time + assert imported_stats_5min[24 - offset]["start"].isoformat() == start_time assert imported_stats[2]["start"].isoformat() == start_time await client.send_json( { @@ -437,10 +482,11 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): response = await client.receive_json() assert response["success"] assert response["result"] == { - "max": max(stat["max"] for stat in imported_stats_5min[24:]), - "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:]), - "min": min(stat["min"] for stat in imported_stats_5min[24:]), - "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[23]["sum"], + "max": max(stat["max"] for stat in imported_stats_5min[24 - offset :]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24 - offset :]), + "min": min(stat["min"] for stat in imported_stats_5min[24 - offset :]), + "change": imported_stats_5min[-1]["sum"] + - imported_stats_5min[23 - offset]["sum"], } # This should also include imported_statistics[2:] + imported_statistics_5min[36:] @@ -457,10 +503,11 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): response = await client.receive_json() assert response["success"] assert response["result"] == { - "max": max(stat["max"] for stat in imported_stats_5min[24:]), - "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:]), - "min": min(stat["min"] for stat in imported_stats_5min[24:]), - "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[23]["sum"], + "max": max(stat["max"] for stat in imported_stats_5min[24 - offset :]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24 - offset :]), + "min": min(stat["min"] for stat in imported_stats_5min[24 - offset :]), + "change": imported_stats_5min[-1]["sum"] + - imported_stats_5min[23 - offset]["sum"], } # This should include imported_statistics[2:3] @@ -477,11 +524,16 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): ) response = await client.receive_json() assert response["success"] + slice_start = 24 - offset + slice_end = 36 - offset assert response["result"] == { - "max": max(stat["max"] for stat in imported_stats_5min[24:36]), - "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:36]), - "min": min(stat["min"] for stat in imported_stats_5min[24:36]), - "change": imported_stats_5min[35]["sum"] - imported_stats_5min[23]["sum"], + "max": max(stat["max"] for stat in imported_stats_5min[slice_start:slice_end]), + "mean": fmean( + stat["mean"] for stat in imported_stats_5min[slice_start:slice_end] + ), + "min": min(stat["min"] for stat in imported_stats_5min[slice_start:slice_end]), + "change": imported_stats_5min[slice_end - 1]["sum"] + - imported_stats_5min[slice_start - 1]["sum"], } # Test we can get only selected types @@ -539,6 +591,167 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } +@freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) +async def test_statistic_during_period_hole(recorder_mock, hass, hass_ws_client): + """Test statistic_during_period when there are holes in the data.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + now = dt_util.utcnow() + + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + zero = now + start = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=-18) + + imported_stats = [ + { + "start": (start + timedelta(hours=3 * i)), + "max": i * 2, + "mean": i, + "min": -76 + i * 2, + "sum": i, + } + for i in range(0, 6) + ] + + imported_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "recorder", + "statistic_id": "sensor.test", + "unit_of_measurement": "kWh", + } + + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats, + Statistics, + ) + await async_wait_recording_done(hass) + + # This should include imported_stats[:] + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[:]), + "mean": fmean(stat["mean"] for stat in imported_stats[:]), + "min": min(stat["min"] for stat in imported_stats[:]), + "change": imported_stats[-1]["sum"] - imported_stats[0]["sum"], + } + + # This should also include imported_stats[:] + start_time = "2022-10-20T13:00:00+00:00" + end_time = "2022-10-21T05:00:00+00:00" + assert imported_stats[0]["start"].isoformat() == start_time + assert imported_stats[-1]["start"].isoformat() < end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[:]), + "mean": fmean(stat["mean"] for stat in imported_stats[:]), + "min": min(stat["min"] for stat in imported_stats[:]), + "change": imported_stats[-1]["sum"] - imported_stats[0]["sum"], + } + + # This should also include imported_stats[:] + start_time = "2022-10-20T13:00:00+00:00" + end_time = "2022-10-21T08:20:00+00:00" + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[:]), + "mean": fmean(stat["mean"] for stat in imported_stats[:]), + "min": min(stat["min"] for stat in imported_stats[:]), + "change": imported_stats[-1]["sum"] - imported_stats[0]["sum"], + } + + # This should include imported_stats[1:4] + start_time = "2022-10-20T16:00:00+00:00" + end_time = "2022-10-20T23:00:00+00:00" + assert imported_stats[1]["start"].isoformat() == start_time + assert imported_stats[3]["start"].isoformat() < end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[1:4]), + "mean": fmean(stat["mean"] for stat in imported_stats[1:4]), + "min": min(stat["min"] for stat in imported_stats[1:4]), + "change": imported_stats[3]["sum"] - imported_stats[1]["sum"], + } + + # This should also include imported_stats[1:4] + start_time = "2022-10-20T15:00:00+00:00" + end_time = "2022-10-21T00:00:00+00:00" + assert imported_stats[1]["start"].isoformat() > start_time + assert imported_stats[3]["start"].isoformat() < end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[1:4]), + "mean": fmean(stat["mean"] for stat in imported_stats[1:4]), + "min": min(stat["min"] for stat in imported_stats[1:4]), + "change": imported_stats[3]["sum"] - imported_stats[1]["sum"], + } + + @freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) @pytest.mark.parametrize( "calendar_period, start_time, end_time", From 1ecb7ab88751e3d1edc86c3ac62c4922eaee0edb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 11 Nov 2022 17:08:26 +0100 Subject: [PATCH 11/41] Fix rest schema (#81857) --- homeassistant/components/rest/schema.py | 3 +-- tests/components/rest/test_init.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index d124ce5789c..cfd8f8a3852 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -91,9 +91,8 @@ COMBINED_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - # convert empty dict to empty list - lambda x: [] if x == {} else x, cv.ensure_list, + cv.remove_falsy, [COMBINED_SCHEMA], ) }, diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 6f35301b1c7..0c1670315fc 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -418,3 +418,19 @@ async def test_empty_config(hass: HomeAssistant) -> None: {DOMAIN: {}}, ) assert_setup_component(0, DOMAIN) + + +async def test_config_schema_via_packages(hass: HomeAssistant) -> None: + """Test configuration via packages.""" + packages = { + "pack_dict": {"rest": {}}, + "pack_11": {"rest": {"resource": "http://url1"}}, + "pack_list": {"rest": [{"resource": "http://url2"}]}, + } + config = {hass_config.CONF_CORE: {hass_config.CONF_PACKAGES: packages}} + await hass_config.merge_packages_config(hass, config, packages) + + assert len(config) == 2 + assert len(config["rest"]) == 2 + assert config["rest"][0]["resource"] == "http://url1" + assert config["rest"][1]["resource"] == "http://url2" From 930dc3615e8b8dcd37b76ebb7e09e85ffb970eb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 10:01:12 -0600 Subject: [PATCH 12/41] Bump aiohomekit to 2.2.19 (#81867) --- 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 b18f35390b7..f0438a7b841 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.18"], + "requirements": ["aiohomekit==2.2.19"], "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 79643e571d5..f2531e17d91 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.18 +aiohomekit==2.2.19 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ab4ae3bd9a..c013403b65e 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.18 +aiohomekit==2.2.19 # homeassistant.components.emulated_hue # homeassistant.components.http From c8177f48cea2064733e5f67c274fa7f85d3646ae Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 9 Nov 2022 15:27:36 +0000 Subject: [PATCH 13/41] Fix homekit_controller climate entity not becoming active when changing modes (#81868) --- .../components/homekit_controller/climate.py | 1 + .../homekit_controller/test_climate.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index de42243a6bb..41e88725121 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -209,6 +209,7 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): ) await self.async_put_characteristics( { + CharacteristicsTypes.ACTIVE: ActivationStateValues.ACTIVE, CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TARGET_HEATER_COOLER_STATE_HASS_TO_HOMEKIT[ hvac_mode ], diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 0f10f0f9fa0..bf544c5aff4 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -760,6 +760,42 @@ async def test_heater_cooler_change_thermostat_state(hass, utcnow): ) +async def test_can_turn_on_after_off(hass, utcnow): + """ + Test that we always force device from inactive to active when setting mode. + + This is a regression test for #81863. + """ + helper = await setup_test_component(hass, create_heater_cooler_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVACMode.OFF}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ACTIVE: ActivationStateValues.INACTIVE, + }, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVACMode.HEAT}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ACTIVE: ActivationStateValues.ACTIVE, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.HEAT, + }, + ) + + async def test_heater_cooler_change_thermostat_temperature(hass, utcnow): """Test that we can change the target temperature.""" helper = await setup_test_component(hass, create_heater_cooler_service) From 5488e9d5f3cf47ba37bdef8acdc62d84720074e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 10:21:31 -0600 Subject: [PATCH 14/41] Bump oralb-ble to 0.14.1 (#81869) --- 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 1738558770e..eff6c999c30 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.13.0"], + "requirements": ["oralb-ble==0.14.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index f2531e17d91..acf76565c6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1241,7 +1241,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.13.0 +oralb-ble==0.14.1 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c013403b65e..ddc2d2f78dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -886,7 +886,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.13.0 +oralb-ble==0.14.1 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 70b360b1f8b3efa1b2a5e3eb7707c866160d77ad Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 9 Nov 2022 19:34:31 -0800 Subject: [PATCH 15/41] Bump gcal_sync to 4.0.1 to fix Google Calendar config flow (#81873) Bump gcal_sync to 4.0.1 This reverts test chagnes from PR #81562 that were actually incorrect given the calendar "get" API returns less information that the "CalendarList" api. --- 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 9fc265fa287..2bc84827cd6 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==4.0.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.0.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index acf76565c6f..9ee94199fa4 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==4.0.0 +gcal-sync==4.0.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddc2d2f78dd..b6c0fc50c20 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==4.0.0 +gcal-sync==4.0.1 # 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 bce3f4855c7..d8ddd6fe588 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", "accessRole": "owner"}, + {"id": primary_calendar_email, "summary": "Personal"}, exc=primary_calendar_error, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index a2f16f778fd..5e7696eec68 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", "accessRole": "reader"}, + {"id": EMAIL_ADDRESS, "summary": "Personal"}, ) mock_calendars_list({"items": [test_api_calendar]}) From 082d4079ef64ac4116dcff198d6d9060b72c557f Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 13 Nov 2022 08:30:16 -0500 Subject: [PATCH 16/41] Fix ZHA configuration APIs (#81874) * Fix ZHA configuration loading and saving issues * add tests --- homeassistant/components/zha/api.py | 10 +- homeassistant/components/zha/core/helpers.py | 4 +- tests/components/zha/data.py | 153 +++++++++++++++++++ tests/components/zha/test_api.py | 75 +++++++++ 4 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 tests/components/zha/data.py diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index c68136c23da..eb5fc2e4343 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1090,11 +1090,17 @@ async def websocket_update_zha_configuration( ): data_to_save[CUSTOM_CONFIGURATION][section].pop(entry) # remove entire section block if empty - if not data_to_save[CUSTOM_CONFIGURATION][section]: + if ( + not data_to_save[CUSTOM_CONFIGURATION].get(section) + and section in data_to_save[CUSTOM_CONFIGURATION] + ): data_to_save[CUSTOM_CONFIGURATION].pop(section) # remove entire custom_configuration block if empty - if not data_to_save[CUSTOM_CONFIGURATION]: + if ( + not data_to_save.get(CUSTOM_CONFIGURATION) + and CUSTOM_CONFIGURATION in data_to_save + ): data_to_save.pop(CUSTOM_CONFIGURATION) _LOGGER.info( diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 1ea9a2a4c9b..2bc7d53fd79 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -221,11 +221,13 @@ def async_get_zha_config_value( ) -def async_cluster_exists(hass, cluster_id): +def async_cluster_exists(hass, cluster_id, skip_coordinator=True): """Determine if a device containing the specified in cluster is paired.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] zha_devices = zha_gateway.devices.values() for zha_device in zha_devices: + if skip_coordinator and zha_device.is_coordinator: + continue clusters_by_endpoint = zha_device.async_get_clusters() for clusters in clusters_by_endpoint.values(): if ( diff --git a/tests/components/zha/data.py b/tests/components/zha/data.py new file mode 100644 index 00000000000..8b613ec2971 --- /dev/null +++ b/tests/components/zha/data.py @@ -0,0 +1,153 @@ +"""Test data for ZHA API tests.""" + +BASE_CUSTOM_CONFIGURATION = { + "schemas": { + "zha_options": [ + { + "type": "integer", + "valueMin": 0, + "name": "default_light_transition", + "optional": True, + "default": 0, + }, + { + "type": "boolean", + "name": "enhanced_light_transition", + "required": True, + "default": False, + }, + { + "type": "boolean", + "name": "light_transitioning_flag", + "required": True, + "default": True, + }, + { + "type": "boolean", + "name": "always_prefer_xy_color_mode", + "required": True, + "default": True, + }, + { + "type": "boolean", + "name": "enable_identify_on_join", + "required": True, + "default": True, + }, + { + "type": "integer", + "valueMin": 0, + "name": "consider_unavailable_mains", + "optional": True, + "default": 7200, + }, + { + "type": "integer", + "valueMin": 0, + "name": "consider_unavailable_battery", + "optional": True, + "default": 21600, + }, + ] + }, + "data": { + "zha_options": { + "enhanced_light_transition": True, + "default_light_transition": 0, + "light_transitioning_flag": True, + "always_prefer_xy_color_mode": True, + "enable_identify_on_join": True, + "consider_unavailable_mains": 7200, + "consider_unavailable_battery": 21600, + } + }, +} + +CONFIG_WITH_ALARM_OPTIONS = { + "schemas": { + "zha_options": [ + { + "type": "integer", + "valueMin": 0, + "name": "default_light_transition", + "optional": True, + "default": 0, + }, + { + "type": "boolean", + "name": "enhanced_light_transition", + "required": True, + "default": False, + }, + { + "type": "boolean", + "name": "light_transitioning_flag", + "required": True, + "default": True, + }, + { + "type": "boolean", + "name": "always_prefer_xy_color_mode", + "required": True, + "default": True, + }, + { + "type": "boolean", + "name": "enable_identify_on_join", + "required": True, + "default": True, + }, + { + "type": "integer", + "valueMin": 0, + "name": "consider_unavailable_mains", + "optional": True, + "default": 7200, + }, + { + "type": "integer", + "valueMin": 0, + "name": "consider_unavailable_battery", + "optional": True, + "default": 21600, + }, + ], + "zha_alarm_options": [ + { + "type": "string", + "name": "alarm_master_code", + "required": True, + "default": "1234", + }, + { + "type": "integer", + "valueMin": 0, + "name": "alarm_failed_tries", + "required": True, + "default": 3, + }, + { + "type": "boolean", + "name": "alarm_arm_requires_code", + "required": True, + "default": False, + }, + ], + }, + "data": { + "zha_options": { + "enhanced_light_transition": True, + "default_light_transition": 0, + "light_transitioning_flag": True, + "always_prefer_xy_color_mode": True, + "enable_identify_on_join": True, + "consider_unavailable_mains": 7200, + "consider_unavailable_battery": 21600, + }, + "zha_alarm_options": { + "alarm_arm_requires_code": False, + "alarm_master_code": "4321", + "alarm_failed_tries": 2, + }, + }, +} diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index e4daf7f365e..defc9842b01 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,5 +1,6 @@ """Test ZHA API.""" from binascii import unhexlify +from copy import deepcopy from unittest.mock import AsyncMock, patch import pytest @@ -8,6 +9,7 @@ import zigpy.backups import zigpy.profiles.zha import zigpy.types import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.security as security from homeassistant.components.websocket_api import const from homeassistant.components.zha import DOMAIN @@ -50,6 +52,7 @@ from .conftest import ( SIG_EP_PROFILE, SIG_EP_TYPE, ) +from .data import BASE_CUSTOM_CONFIGURATION, CONFIG_WITH_ALARM_OPTIONS IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7" IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @@ -61,6 +64,7 @@ def required_platform_only(): with patch( "homeassistant.components.zha.PLATFORMS", ( + Platform.ALARM_CONTROL_PANEL, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, @@ -89,6 +93,25 @@ async def device_switch(hass, zigpy_device_mock, zha_device_joined): return zha_device +@pytest.fixture +async def device_ias_ace(hass, zigpy_device_mock, zha_device_joined): + """Test alarm control panel device.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [security.IasAce.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.IAS_ANCILLARY_CONTROL, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } + }, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.available = True + return zha_device + + @pytest.fixture async def device_groupable(hass, zigpy_device_mock, zha_device_joined): """Test zha light platform.""" @@ -225,6 +248,58 @@ async def test_list_devices(zha_client): assert device == device2 +async def test_get_zha_config(zha_client): + """Test getting zha custom configuration.""" + await zha_client.send_json({ID: 5, TYPE: "zha/configuration"}) + + msg = await zha_client.receive_json() + + configuration = msg["result"] + assert configuration == BASE_CUSTOM_CONFIGURATION + + +async def test_get_zha_config_with_alarm(hass, zha_client, device_ias_ace): + """Test getting zha custom configuration.""" + await zha_client.send_json({ID: 5, TYPE: "zha/configuration"}) + + msg = await zha_client.receive_json() + + configuration = msg["result"] + assert configuration == CONFIG_WITH_ALARM_OPTIONS + + # test that the alarm options are not in the config when we remove the device + device_ias_ace.gateway.device_removed(device_ias_ace.device) + await hass.async_block_till_done() + await zha_client.send_json({ID: 6, TYPE: "zha/configuration"}) + + msg = await zha_client.receive_json() + + configuration = msg["result"] + assert configuration == BASE_CUSTOM_CONFIGURATION + + +async def test_update_zha_config(zha_client, zigpy_app_controller): + """Test updating zha custom configuration.""" + + configuration = deepcopy(CONFIG_WITH_ALARM_OPTIONS) + configuration["data"]["zha_options"]["default_light_transition"] = 10 + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ): + await zha_client.send_json( + {ID: 5, TYPE: "zha/configuration/update", "data": configuration["data"]} + ) + msg = await zha_client.receive_json() + assert msg["success"] + + await zha_client.send_json({ID: 6, TYPE: "zha/configuration"}) + msg = await zha_client.receive_json() + configuration = msg["result"] + assert configuration == configuration + + async def test_device_not_found(zha_client): """Test not found response from get device API.""" await zha_client.send_json( From 223d864b04368f252c42e7d3eeda16da0e50f261 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 10 Nov 2022 08:31:28 -0800 Subject: [PATCH 17/41] Revert google calendar back to old API for free/busy readers (#81894) * Revert google calendar back to old API for free/busy readers * Update homeassistant/components/google/calendar.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/google/calendar.py | 10 ++++++++-- tests/components/google/conftest.py | 14 +++++++++++--- tests/components/google/test_calendar.py | 21 ++++++++++++++++----- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 4eb57cff49c..eff26c2fbc4 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -10,7 +10,7 @@ from typing import Any from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest from gcal_sync.exceptions import ApiException -from gcal_sync.model import DateOrDatetime, Event +from gcal_sync.model import AccessRole, DateOrDatetime, Event from gcal_sync.store import ScopedCalendarStore from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.timeline import Timeline @@ -198,7 +198,13 @@ async def async_setup_entry( entity_entry.entity_id, ) coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator - if search := data.get(CONF_SEARCH): + # Prefer calendar sync down of resources when possible. However, sync does not work + # for search. Also free-busy calendars denormalize recurring events as individual + # events which is not efficient for sync + if ( + search := data.get(CONF_SEARCH) + or calendar_item.access_role == AccessRole.FREE_BUSY_READER + ): coordinator = CalendarQueryUpdateCoordinator( hass, calendar_service, diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 2f5efd829bf..ad27e971ece 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -47,7 +47,6 @@ TEST_API_CALENDAR = { "id": CALENDAR_ID, "etag": '"3584134138943410"', "timeZone": "UTC", - "accessRole": "reader", "foregroundColor": "#000000", "selected": True, "kind": "calendar#calendarListEntry", @@ -62,10 +61,19 @@ CLIENT_ID = "client-id" CLIENT_SECRET = "client-secret" +@pytest.fixture(name="calendar_access_role") +def test_calendar_access_role() -> str: + """Default access role to use for test_api_calendar in tests.""" + return "reader" + + @pytest.fixture -def test_api_calendar(): +def test_api_calendar(calendar_access_role: str): """Return a test calendar object used in API responses.""" - return TEST_API_CALENDAR + return { + **TEST_API_CALENDAR, + "accessRole": calendar_access_role, + } @pytest.fixture diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 3bd584f4c6f..c813bd55782 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -60,6 +60,14 @@ TEST_EVENT = { } +@pytest.fixture( + autouse=True, scope="module", params=["reader", "owner", "freeBusyReader"] +) +def calendar_access_role(request) -> str: + """Fixture to exercise access roles in tests.""" + return request.param + + @pytest.fixture(autouse=True) def mock_test_setup( hass, @@ -724,12 +732,15 @@ async def test_invalid_unique_id_cleanup( @pytest.mark.parametrize( - "time_zone,event_order", + "time_zone,event_order,calendar_access_role", + # This only tests the reader role to force testing against the local + # database filtering based on start/end time. (free busy reader would + # just use the API response which this test is not exercising) [ - ("America/Los_Angeles", ["One", "Two", "All Day Event"]), - ("America/Regina", ["One", "Two", "All Day Event"]), - ("UTC", ["One", "All Day Event", "Two"]), - ("Asia/Tokyo", ["All Day Event", "One", "Two"]), + ("America/Los_Angeles", ["One", "Two", "All Day Event"], "reader"), + ("America/Regina", ["One", "Two", "All Day Event"], "reader"), + ("UTC", ["One", "All Day Event", "Two"], "reader"), + ("Asia/Tokyo", ["All Day Event", "One", "Two"], "reader"), ], ) async def test_all_day_iter_order( From 0d62d800388272bf62953f2c94de9836bcc743f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Nov 2022 14:14:37 -0600 Subject: [PATCH 18/41] Fix bluetooth adapters with missing firmware patch files not being discovered (#81926) --- .../components/bluetooth/__init__.py | 25 +++++- homeassistant/components/bluetooth/const.py | 9 +++ tests/components/bluetooth/test_init.py | 77 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 1d0b8824fb5..8590d1ad90a 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from asyncio import Future from collections.abc import Callable, Iterable +import datetime import logging import platform from typing import TYPE_CHECKING, cast @@ -21,6 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_ca from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, @@ -33,6 +35,7 @@ from .const import ( ADAPTER_ADDRESS, ADAPTER_HW_VERSION, ADAPTER_SW_VERSION, + BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, @@ -40,6 +43,7 @@ from .const import ( DEFAULT_ADDRESS, DOMAIN, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, SOURCE_LOCAL, AdapterDetails, ) @@ -298,9 +302,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_discover_adapters(hass, discovered_adapters) discovery_debouncer = Debouncer( - hass, _LOGGER, cooldown=5, immediate=False, function=_async_rediscover_adapters + hass, + _LOGGER, + cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, + immediate=False, + function=_async_rediscover_adapters, ) + async def _async_call_debouncer(now: datetime.datetime) -> None: + """Call the debouncer at a later time.""" + await discovery_debouncer.async_call() + def _async_trigger_discovery() -> None: # There are so many bluetooth adapter models that # we check the bus whenever a usb device is plugged in @@ -310,6 +322,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # present. _LOGGER.debug("Triggering bluetooth usb discovery") hass.async_create_task(discovery_debouncer.async_call()) + # Because it can take 120s for the firmware loader + # fallback to timeout we need to wait that plus + # the debounce time to ensure we do not miss the + # adapter becoming available to DBus since otherwise + # we will never see the new adapter until + # Home Assistant is restarted + async_call_later( + hass, + BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, + _async_call_debouncer, + ) cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery) hass.bus.async_listen_once( diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 6d6751f6ac4..038c2b1988f 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -59,6 +59,15 @@ SCANNER_WATCHDOG_TIMEOUT: Final = 90 SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30) +# When the linux kernel is configured with +# CONFIG_FW_LOADER_USER_HELPER_FALLBACK it +# can take up to 120s before the USB device +# is available if the firmware files +# are not present +LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS = 120 +BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS = 5 + + class AdapterDetails(TypedDict, total=False): """Adapter details.""" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index c9a5e6c78a7..5a5437af71a 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -20,9 +20,11 @@ from homeassistant.components.bluetooth import ( scanner, ) from homeassistant.components.bluetooth.const import ( + BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_PASSIVE, DEFAULT_ADDRESS, DOMAIN, + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) @@ -2737,6 +2739,81 @@ async def test_discover_new_usb_adapters(hass, mock_bleak_scanner_start, one_ada assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 +async def test_discover_new_usb_adapters_with_firmware_fallback_delay( + hass, mock_bleak_scanner_start, one_adapter +): + """Test we can discover new usb adapters with a firmware fallback delay.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_scan_request_callback(_hass, _callback): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.usb.async_register_scan_request_callback", + _async_register_scan_request_callback, + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + + assert not hass.config_entries.flow.async_progress(DOMAIN) + + saved_callback() + assert not hass.config_entries.flow.async_progress(DOMAIN) + + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2) + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 0 + + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + "hci1": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:02", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta( + seconds=LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS + + (BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2) + ), + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 + + async def test_issue_outdated_haos( hass, mock_bleak_scanner_start, one_adapter, operating_system_85 ): From f24549f7d167185054a2d5800bba7760b4821ab4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 10 Nov 2022 14:32:49 -0700 Subject: [PATCH 19/41] Bump aioridwell to 2022.11.0 (#81929) --- homeassistant/components/ridwell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index aec0faf5dd3..785457a57e0 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -3,7 +3,7 @@ "name": "Ridwell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ridwell", - "requirements": ["aioridwell==2022.03.0"], + "requirements": ["aioridwell==2022.11.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["aioridwell"], diff --git a/requirements_all.txt b/requirements_all.txt index 9ee94199fa4..858194c739a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ aioqsw==0.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.03.0 +aioridwell==2022.11.0 # homeassistant.components.senseme aiosenseme==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6c0fc50c20..3c6ab32dc73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -221,7 +221,7 @@ aioqsw==0.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.03.0 +aioridwell==2022.11.0 # homeassistant.components.senseme aiosenseme==0.6.1 From a9d461a109ae49bdf490ad5840da6872b1362464 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 02:09:28 -0600 Subject: [PATCH 20/41] Fix esphome bleak client seeing disconnects too late (#81932) * Fix esphome bleak client seeing disconnects too late Because allbacks are delivered asynchronously its possible that we find out during the operation before the callback is delivered telling us about the disconnected. We now watch for error code -1 which indicates the connection dropped out from under us * debug logging * cleanup comment * Fix comment grammar Co-authored-by: Martin Hjelmare --- .../components/esphome/bluetooth/client.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index c6b60831577..ceac4e5aaae 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -13,6 +13,7 @@ from aioesphomeapi import ( BLEConnectionError, ) from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError +from aioesphomeapi.core import BluetoothGATTAPIError import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback @@ -83,6 +84,24 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: return await func(self, *args, **kwargs) except TimeoutAPIError as err: raise asyncio.TimeoutError(str(err)) from err + except BluetoothGATTAPIError as ex: + # If the device disconnects in the middle of an operation + # be sure to mark it as disconnected so any library using + # the proxy knows to reconnect. + # + # Because callbacks are delivered asynchronously it's possible + # that we find out about the disconnection during the operation + # before the callback is delivered. + if ex.error.error == -1: + _LOGGER.debug( + "%s: %s - %s: BLE device disconnected during %s operation", + self._source, # pylint: disable=protected-access + self._ble_device.name, # pylint: disable=protected-access + self._ble_device.address, # pylint: disable=protected-access + func.__name__, + ) + self._async_ble_device_disconnected() # pylint: disable=protected-access + raise BleakError(str(ex)) from ex except APIConnectionError as err: raise BleakError(str(err)) from err From 97929bd234543797701c01707ef934aaa9ce9764 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 01:51:58 -0600 Subject: [PATCH 21/41] Bump bleak-retry-connector to 2.8.4 (#81937) changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.3...v2.8.4 reduces the risk that the operation will fail because the adapter temporarily runs out of connection slots --- 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 2e038cda76e..2b6847b65b8 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.2", - "bleak-retry-connector==2.8.3", + "bleak-retry-connector==2.8.4", "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 9db814d2dea..69eb6f4976c 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.3 +bleak-retry-connector==2.8.4 bleak==0.19.2 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 858194c739a..21a4d97fabb 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.3 +bleak-retry-connector==2.8.4 # homeassistant.components.bluetooth bleak==0.19.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c6ab32dc73..bf913bcfa3d 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.3 +bleak-retry-connector==2.8.4 # homeassistant.components.bluetooth bleak==0.19.2 From 8f3449d942254e40593ee2ba856e9e46b309e159 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 01:53:07 -0600 Subject: [PATCH 22/41] Bump PySwitchbot to 0.20.3 (#81938) changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.20.2...0.20.3 releases the connection sooner to reduce the risk of running out of connection slots on the ble adapter --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 532edac7d43..2c95327beef 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.20.2"], + "requirements": ["PySwitchbot==0.20.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 21a4d97fabb..ec9f1535666 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.2 +PySwitchbot==0.20.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf913bcfa3d..c4ba49a917f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.2 +PySwitchbot==0.20.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 0457a744284e427761ee3ece867d239fc5cd8356 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 16 Nov 2022 11:51:14 +0000 Subject: [PATCH 23/41] Fix ONVIF subscription errors (#81965) fixes undefined --- homeassistant/components/onvif/event.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 3801d8081db..2dd5d226e37 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -8,7 +8,7 @@ import datetime as dt from httpx import RemoteProtocolError, TransportError from onvif import ONVIFCamera, ONVIFService -from zeep.exceptions import Fault +from zeep.exceptions import Fault, XMLParseError from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.helpers.event import async_call_later @@ -20,6 +20,7 @@ from .parsers import PARSERS UNHANDLED_TOPICS = set() SUBSCRIPTION_ERRORS = ( + XMLParseError, Fault, asyncio.TimeoutError, TransportError, @@ -153,7 +154,8 @@ class EventManager: .isoformat(timespec="seconds") .replace("+00:00", "Z") ) - await self._subscription.Renew(termination_time) + with suppress(*SUBSCRIPTION_ERRORS): + await self._subscription.Renew(termination_time) def async_schedule_pull(self) -> None: """Schedule async_pull_messages to run.""" From 4e82f134b2c4afe7706e39700d1252b5a08bddd8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 11 Nov 2022 17:03:32 -0500 Subject: [PATCH 24/41] Bump ZHA quirks lib to 0.0.86 (#81966) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c8aebe3b0c0..312b93aff6f 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.85", + "zha-quirks==0.0.86", "zigpy-deconz==0.19.0", "zigpy==0.51.5", "zigpy-xbee==0.16.2", diff --git a/requirements_all.txt b/requirements_all.txt index ec9f1535666..7a8443fd382 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2610,7 +2610,7 @@ zengge==0.2 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.85 +zha-quirks==0.0.86 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4ba49a917f..5afffcd0b3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1811,7 +1811,7 @@ zamg==0.1.1 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.85 +zha-quirks==0.0.86 # homeassistant.components.zha zigpy-deconz==0.19.0 From 5306b32a48aeb53f9a821b93503a562dd979ac2f Mon Sep 17 00:00:00 2001 From: Jeef Date: Tue, 15 Nov 2022 09:29:32 -0800 Subject: [PATCH 25/41] Increasing device usage update interval for Flume (#81968) --- homeassistant/components/flume/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index 2d53db4c486..b9192207e75 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -17,7 +17,7 @@ DEFAULT_NAME = "Flume Sensor" # Flume API limits individual endpoints to 120 queries per hour NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=1) -DEVICE_SCAN_INTERVAL = timedelta(minutes=1) +DEVICE_SCAN_INTERVAL = timedelta(minutes=5) DEVICE_CONNECTION_SCAN_INTERVAL = timedelta(minutes=1) _LOGGER = logging.getLogger(__package__) From 3bf3a1fd8599aa2ad3a5b1c89a7258487afaa050 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 16:01:54 -0600 Subject: [PATCH 26/41] Bump oralb-ble to 0.14.2 (#81969) fixes detection of the black 9000 model fixes #81967 changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.14.1...v0.14.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 eff6c999c30..8868330a7e7 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.14.1"], + "requirements": ["oralb-ble==0.14.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 7a8443fd382..3ba9c7941ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1241,7 +1241,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.14.1 +oralb-ble==0.14.2 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5afffcd0b3e..281ffd05ae3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -886,7 +886,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.14.1 +oralb-ble==0.14.2 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 533efa288024f12048bbed22fbb8e63bf9032069 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 13 Nov 2022 06:58:59 -0800 Subject: [PATCH 27/41] Bump gcal_sync 4.0.2 (#82017) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 2bc84827cd6..7de3a735b96 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==4.0.1", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.0.2", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 3ba9c7941ab..43037eddc37 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==4.0.1 +gcal-sync==4.0.2 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 281ffd05ae3..9e73316565e 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==4.0.1 +gcal-sync==4.0.2 # homeassistant.components.geocaching geocachingapi==0.2.1 From 76cc26ad17095edb5a385b292e999a49789d1caf Mon Sep 17 00:00:00 2001 From: Vincent Giorgi Date: Sun, 13 Nov 2022 16:11:07 +0100 Subject: [PATCH 28/41] Bump airthings-ble to 0.5.3 (#82033) Bump airthings-ble --- homeassistant/components/airthings_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index dca2dbbb562..422a51c7187 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Airthings BLE", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airthings_ble", - "requirements": ["airthings-ble==0.5.2"], + "requirements": ["airthings-ble==0.5.3"], "dependencies": ["bluetooth"], "codeowners": ["@vincegio"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 43037eddc37..221ca65c7d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,7 +294,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.5.2 +airthings-ble==0.5.3 # homeassistant.components.airthings airthings_cloud==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e73316565e..95f1983f464 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,7 +269,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.5.2 +airthings-ble==0.5.3 # homeassistant.components.airthings airthings_cloud==0.1.0 From 609438d929662e9921a04f723f14b863f2f2e0d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Nov 2022 11:27:59 -0600 Subject: [PATCH 29/41] Make sure the config_flow key is set for brands (#82038) Fixes https://github.com/home-assistant/frontend/issues/14376 --- homeassistant/generated/integrations.json | 85 +++++++++++++++++++++++ script/hassfest/config_flow.py | 5 +- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 08317d06a5c..b917153203b 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -159,21 +159,25 @@ "integrations": { "alexa": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Amazon Alexa" }, "amazon_polly": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Amazon Polly" }, "aws": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Amazon Web Services (AWS)" }, "route53": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "AWS Route53" } @@ -284,6 +288,7 @@ }, "itunes": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Apple iTunes" } @@ -336,11 +341,13 @@ "integrations": { "aruba": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Aruba" }, "cppm_tracker": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Aruba ClearPass" } @@ -363,11 +370,13 @@ "integrations": { "asterisk_cdr": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Asterisk Call Detail Records" }, "asterisk_mbox": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Asterisk Voicemail" } @@ -710,16 +719,19 @@ "integrations": { "cisco_ios": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Cisco IOS" }, "cisco_mobility_express": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Cisco Mobility Express" }, "cisco_webex_teams": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Cisco Webex Teams" } @@ -748,11 +760,13 @@ "integrations": { "clicksend": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "ClickSend SMS" }, "clicksend_tts": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "ClickSend TTS" } @@ -944,6 +958,7 @@ "integrations": { "denon": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Denon Network Receivers" }, @@ -1245,6 +1260,7 @@ "integrations": { "avea": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Elgato Avea" }, @@ -1291,11 +1307,13 @@ "integrations": { "emoncms": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Emoncms" }, "emoncms_history": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Emoncms History" } @@ -1377,6 +1395,7 @@ }, "epsonworkforce": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Epson Workforce" } @@ -1387,11 +1406,13 @@ "integrations": { "eq3btsmart": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "eQ-3 Bluetooth Smart Thermostats" }, "maxcube": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "eQ-3 MAX!" } @@ -1480,15 +1501,18 @@ "integrations": { "ffmpeg": { "integration_type": "hub", + "config_flow": false, "name": "FFmpeg" }, "ffmpeg_motion": { "integration_type": "hub", + "config_flow": false, "iot_class": "calculated", "name": "FFmpeg Motion" }, "ffmpeg_noise": { "integration_type": "hub", + "config_flow": false, "iot_class": "calculated", "name": "FFmpeg Noise" } @@ -1871,11 +1895,13 @@ "integrations": { "gc100": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Global Cach\u00e9 GC-100" }, "itach": { "integration_type": "hub", + "config_flow": false, "iot_class": "assumed_state", "name": "Global Cach\u00e9 iTach TCP/IP to IR" } @@ -1910,26 +1936,31 @@ "integrations": { "google_assistant": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Google Assistant" }, "google_cloud": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Google Cloud Platform" }, "google_domains": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Google Domains" }, "google_maps": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Google Maps" }, "google_pubsub": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Google Pub/Sub" }, @@ -1941,6 +1972,7 @@ }, "google_translate": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Google Translate Text-to-Speech" }, @@ -1951,6 +1983,7 @@ }, "google_wifi": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Google Wifi" }, @@ -2119,11 +2152,13 @@ "integrations": { "hikvision": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Hikvision" }, "hikvisioncam": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Hikvision" } @@ -2176,6 +2211,7 @@ "integrations": { "homematic": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Homematic" }, @@ -2204,6 +2240,7 @@ }, "evohome": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Honeywell Total Connect Comfort (Europe)" }, @@ -2297,11 +2334,13 @@ "integrations": { "watson_iot": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "IBM Watson IoT Platform" }, "watson_tts": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "IBM Watson TTS" } @@ -2342,6 +2381,7 @@ "integrations": { "symfonisk": { "integration_type": "virtual", + "config_flow": false, "supported_by": "sonos", "name": "IKEA SYMFONISK" }, @@ -2720,6 +2760,7 @@ "integrations": { "lg_netcast": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "LG Netcast" }, @@ -2855,6 +2896,7 @@ }, "ue_smart_radio": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Logitech UE Smart Radio" }, @@ -2901,6 +2943,7 @@ "integrations": { "lutron": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Lutron" }, @@ -2912,6 +2955,7 @@ }, "homeworks": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Lutron Homeworks" } @@ -3021,6 +3065,7 @@ }, "raincloud": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Melnor RainCloud" } @@ -3097,31 +3142,37 @@ }, "azure_service_bus": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Azure Service Bus" }, "microsoft_face_detect": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Face Detect" }, "microsoft_face_identify": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Face Identify" }, "microsoft_face": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Face" }, "microsoft": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Text-to-Speech (TTS)" }, "msteams": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Teams" }, @@ -3133,6 +3184,7 @@ }, "xbox_live": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Xbox Live" } @@ -3260,6 +3312,7 @@ "integrations": { "manual_mqtt": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Manual MQTT Alarm Control Panel" }, @@ -3271,21 +3324,25 @@ }, "mqtt_eventstream": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "MQTT Eventstream" }, "mqtt_json": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "MQTT JSON" }, "mqtt_room": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "MQTT Room Presence" }, "mqtt_statestream": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "MQTT Statestream" } @@ -3404,6 +3461,7 @@ }, "netgear_lte": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "NETGEAR LTE" } @@ -3765,11 +3823,13 @@ "integrations": { "luci": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "OpenWrt (luci)" }, "ubus": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "OpenWrt (ubus)" } @@ -3846,6 +3906,7 @@ "integrations": { "panasonic_bluray": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Panasonic Blu-Ray Player" }, @@ -4140,6 +4201,7 @@ "integrations": { "qnap": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "QNAP" }, @@ -4228,6 +4290,7 @@ "integrations": { "rpi_camera": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Raspberry Pi Camera" }, @@ -4238,6 +4301,7 @@ }, "remote_rpi_gpio": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Raspberry Pi Remote GPIO" } @@ -4437,11 +4501,13 @@ "integrations": { "russound_rio": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Russound RIO" }, "russound_rnet": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Russound RNET" } @@ -4464,6 +4530,7 @@ "integrations": { "familyhub": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Samsung Family Hub" }, @@ -4845,6 +4912,7 @@ }, "solaredge_local": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "SolarEdge Local" } @@ -4908,6 +4976,7 @@ }, "sony_projector": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Sony Projector" }, @@ -5121,6 +5190,7 @@ "integrations": { "synology_chat": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Synology Chat" }, @@ -5132,6 +5202,7 @@ }, "synology_srm": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Synology SRM" } @@ -5218,11 +5289,13 @@ "integrations": { "telegram": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Telegram" }, "telegram_bot": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Telegram bot" } @@ -5239,6 +5312,7 @@ }, "tellstick": { "integration_type": "hub", + "config_flow": false, "iot_class": "assumed_state", "name": "TellStick" } @@ -5522,11 +5596,13 @@ }, "twilio_call": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Twilio Call" }, "twilio_sms": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Twilio SMS" } @@ -5555,6 +5631,7 @@ "integrations": { "ultraloq": { "integration_type": "virtual", + "config_flow": false, "iot_standards": [ "zwave" ], @@ -5573,11 +5650,13 @@ }, "unifi_direct": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "UniFi AP" }, "unifiled": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "UniFi LED" }, @@ -5754,6 +5833,7 @@ "integrations": { "vlc": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "VLC media player" }, @@ -5978,11 +6058,13 @@ }, "xiaomi_tv": { "integration_type": "hub", + "config_flow": false, "iot_class": "assumed_state", "name": "Xiaomi TV" }, "xiaomi": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Xiaomi" } @@ -6040,11 +6122,13 @@ "integrations": { "yandex_transport": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Yandex Transport" }, "yandextts": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Yandex TTS" } @@ -6061,6 +6145,7 @@ }, "yeelightsunflower": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Yeelight Sunflower" } diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 9cebb37d371..84347697147 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -113,8 +113,9 @@ def _populate_brand_integrations( metadata = { "integration_type": integration.integration_type, } - if integration.config_flow: - metadata["config_flow"] = integration.config_flow + # Always set the config_flow key to avoid breaking the frontend + # https://github.com/home-assistant/frontend/issues/14376 + metadata["config_flow"] = bool(integration.config_flow) if integration.iot_class: metadata["iot_class"] = integration.iot_class if integration.supported_by: From f8b5a97e72f283787189746eb0bcb28073633dab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Nov 2022 17:12:52 -0600 Subject: [PATCH 30/41] Bump pySwitchbot to 0.20.4 (#82055) Fixes for curtain firmware v6 changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.20.3...0.20.4 --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 2c95327beef..d586a328ed7 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.20.3"], + "requirements": ["PySwitchbot==0.20.4"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 221ca65c7d7..c313121c620 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.3 +PySwitchbot==0.20.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95f1983f464..38794ee865c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.3 +PySwitchbot==0.20.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 2a641d1d19bf0d6d64c5d740cec581df487e7d24 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 14 Nov 2022 12:43:45 +1100 Subject: [PATCH 31/41] Restore color_temp handling for lifx.set_state (#82067) fixes undefined --- homeassistant/components/lifx/util.py | 7 +++++++ tests/components/lifx/test_light.py | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index 6a9bff465ee..fde36d714d5 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -16,6 +16,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_KELVIN, @@ -114,6 +115,12 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | kelvin = kwargs.pop(ATTR_KELVIN) saturation = 0 + if ATTR_COLOR_TEMP in kwargs: + kelvin = color_util.color_temperature_mired_to_kelvin( + kwargs.pop(ATTR_COLOR_TEMP) + ) + saturation = 0 + if ATTR_COLOR_TEMP_KELVIN in kwargs: kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN) saturation = 0 diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 5a9b250034a..3bf82fce3fc 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -1524,6 +1524,15 @@ async def test_lifx_set_state_kelvin(hass: HomeAssistant) -> None: assert bulb.set_color.calls[0][0][0] == [32000, 0, 25700, 2700] bulb.set_color.reset_mock() + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255, ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 65535, 2500] + bulb.set_color.reset_mock() + async def test_infrared_color_bulb(hass: HomeAssistant) -> None: """Test setting infrared with a color bulb.""" From 66d3891a376527f26f352a42480df24340cf27e3 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Mon, 14 Nov 2022 09:41:25 +0000 Subject: [PATCH 32/41] Bump growattServer to 1.2.4 (#82071) Growatt - Library bump to workaround for #81951 --- homeassistant/components/growatt_server/manifest.json | 2 +- homeassistant/components/growatt_server/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index f3f17804fc1..e3b63f7c8b3 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.2.3"], + "requirements": ["growattServer==1.2.4"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], "iot_class": "cloud_polling", "loggers": ["growattServer"] diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index eceba2f7bce..ac19d91b24d 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -32,7 +32,7 @@ from .sensor_types.total import TOTAL_SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = datetime.timedelta(minutes=1) +SCAN_INTERVAL = datetime.timedelta(minutes=5) def get_device_list(api, config): diff --git a/requirements_all.txt b/requirements_all.txt index c313121c620..d96b3422382 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -804,7 +804,7 @@ greenwavereality==0.5.1 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.3 +growattServer==1.2.4 # homeassistant.components.google_sheets gspread==5.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38794ee865c..cd003264740 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ greeneye_monitor==3.0.3 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.3 +growattServer==1.2.4 # homeassistant.components.google_sheets gspread==5.5.0 From 431f93e1d3378d1987caec5e12708a6972852479 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Nov 2022 11:30:03 -0600 Subject: [PATCH 33/41] Bump PySwitchbot to 0.20.5 (#82099) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index d586a328ed7..274c5784b2f 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.20.4"], + "requirements": ["PySwitchbot==0.20.5"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d96b3422382..d41703e1c7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.4 +PySwitchbot==0.20.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd003264740..075254c5ad8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.4 +PySwitchbot==0.20.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 57c868e615cd5e84f78884c0286b93ee0e94380b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Nov 2022 15:51:13 +0100 Subject: [PATCH 34/41] Update sqlalchemy to 1.4.44 (#82129) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index afdabfd6d01..3fb873bfc90 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.42", "fnvhash==0.1.0"], + "requirements": ["sqlalchemy==1.4.44", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 7484ca0feb7..4ee1683a357 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.42"], + "requirements": ["sqlalchemy==1.4.44"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 69eb6f4976c..4641c32d562 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -37,7 +37,7 @@ pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 scapy==2.4.5 -sqlalchemy==1.4.42 +sqlalchemy==1.4.44 typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index d41703e1c7d..bbe5c347870 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2314,7 +2314,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.42 +sqlalchemy==1.4.44 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 075254c5ad8..1415f9b4cb6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1599,7 +1599,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.42 +sqlalchemy==1.4.44 # homeassistant.components.srp_energy srpenergy==1.3.6 From 228fa9f5a00812f9773a331672252415a7a61902 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 15 Nov 2022 18:43:01 +0100 Subject: [PATCH 35/41] Always update attributes on an update for MQTT update (#82139) --- homeassistant/components/mqtt/update.py | 8 ++++---- tests/components/mqtt/test_update.py | 16 ++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 5536d16d1c7..abad1cdb2ff 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -196,19 +196,19 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): self._attr_latest_version = json_payload["latest_version"] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if CONF_TITLE in json_payload and not self._attr_title: + if CONF_TITLE in json_payload: self._attr_title = json_payload[CONF_TITLE] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if CONF_RELEASE_SUMMARY in json_payload and not self._attr_release_summary: + if CONF_RELEASE_SUMMARY in json_payload: self._attr_release_summary = json_payload[CONF_RELEASE_SUMMARY] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if CONF_RELEASE_URL in json_payload and not self._attr_release_url: + if CONF_RELEASE_URL in json_payload: self._attr_release_url = json_payload[CONF_RELEASE_URL] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if CONF_ENTITY_PICTURE in json_payload and not self._entity_picture: + if CONF_ENTITY_PICTURE in json_payload: self._entity_picture = json_payload[CONF_ENTITY_PICTURE] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index e7d75ee7cc8..a8f925bf4a6 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -203,8 +203,9 @@ async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config): hass, state_topic, '{"installed_version":"1.9.0","latest_version":"1.9.0",' - '"title":"Test Update Title","release_url":"https://example.com/release",' - '"release_summary":"Test release summary"}', + '"title":"Test Update 1 Title","release_url":"https://example.com/release1",' + '"release_summary":"Test release summary 1",' + '"entity_picture": "https://example.com/icon1.png"}', ) await hass.async_block_till_done() @@ -213,14 +214,16 @@ async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_OFF assert state.attributes.get("installed_version") == "1.9.0" assert state.attributes.get("latest_version") == "1.9.0" - assert state.attributes.get("release_summary") == "Test release summary" - assert state.attributes.get("release_url") == "https://example.com/release" - assert state.attributes.get("title") == "Test Update Title" + assert state.attributes.get("release_summary") == "Test release summary 1" + assert state.attributes.get("release_url") == "https://example.com/release1" + assert state.attributes.get("title") == "Test Update 1 Title" + assert state.attributes.get("entity_picture") == "https://example.com/icon1.png" async_fire_mqtt_message( hass, state_topic, - '{"installed_version":"1.9.0","latest_version":"2.0.0","title":"Test Update Title"}', + '{"installed_version":"1.9.0","latest_version":"2.0.0",' + '"title":"Test Update 2 Title","entity_picture":"https://example.com/icon2.png"}', ) await hass.async_block_till_done() @@ -229,6 +232,7 @@ async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_ON assert state.attributes.get("installed_version") == "1.9.0" assert state.attributes.get("latest_version") == "2.0.0" + assert state.attributes.get("entity_picture") == "https://example.com/icon2.png" async def test_json_state_message_with_template(hass, mqtt_mock_entry_with_yaml_config): From aade51248db3ac353201091822c6fa4259cbd494 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 16 Nov 2022 07:04:34 -0500 Subject: [PATCH 36/41] Add missing strings in Onvif (#82141) --- homeassistant/components/onvif/strings.json | 3 ++- homeassistant/components/onvif/translations/en.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json index 4cf1bd4bad0..210027e96e5 100644 --- a/homeassistant/components/onvif/strings.json +++ b/homeassistant/components/onvif/strings.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Extra FFMPEG arguments", - "rtsp_transport": "RTSP transport mechanism" + "rtsp_transport": "RTSP transport mechanism", + "use_wallclock_as_timestamps": "Use wall clock as timestamps" }, "title": "ONVIF Device Options" } diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json index c3b328646ee..473e2af9ba8 100644 --- a/homeassistant/components/onvif/translations/en.json +++ b/homeassistant/components/onvif/translations/en.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Extra FFMPEG arguments", - "rtsp_transport": "RTSP transport mechanism" + "rtsp_transport": "RTSP transport mechanism", + "use_wallclock_as_timestamps": "Use wall clock as timestamps" }, "title": "ONVIF Device Options" } From 83db9a3335d3ece9b9157f71de11202aa9d2fd87 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 15 Nov 2022 17:07:43 -0500 Subject: [PATCH 37/41] Fix Google Sheets formula input (#82157) --- homeassistant/components/google_sheets/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index e211693bf21..19f5ce81f5c 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -7,6 +7,7 @@ import aiohttp from google.auth.exceptions import RefreshError from google.oauth2.credentials import Credentials from gspread import Client +from gspread.utils import ValueInputOption import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -100,7 +101,7 @@ async def async_setup_service(hass: HomeAssistant) -> None: columns.append(key) worksheet.update_cell(1, len(columns), key) row.append(value) - worksheet.append_row(row) + worksheet.append_row(row, value_input_option=ValueInputOption.user_entered) async def append_to_sheet(call: ServiceCall) -> None: """Append new line of data to a Google Sheets document.""" From 987add50cbf7e94df320abe485fa534ea4d3bceb Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Wed, 16 Nov 2022 09:41:14 +0000 Subject: [PATCH 38/41] Fix Growatt incorrect energy dashboard values for grid import (#82163) * Fix Growatt incorrect energy dashboard values for grid import (#80905) * Growatt - addressing review comments (#80905) * Growatt - addressing more review comments (#80905) --- .../components/growatt_server/sensor.py | 61 +++++++++++++++++-- .../growatt_server/sensor_types/mix.py | 1 + .../sensor_types/sensor_entity_description.py | 1 + 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index ac19d91b24d..d6b74b78475 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -159,7 +159,7 @@ class GrowattInverter(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - result = self.probe.get_data(self.entity_description.api_key) + result = self.probe.get_data(self.entity_description) if self.entity_description.precision is not None: result = round(result, self.entity_description.precision) return result @@ -168,7 +168,7 @@ class GrowattInverter(SensorEntity): def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor, if any.""" if self.entity_description.currency: - return self.probe.get_data("currency") + return self.probe.get_currency() return super().native_unit_of_measurement def update(self) -> None: @@ -187,6 +187,7 @@ class GrowattData: self.device_id = device_id self.plant_id = None self.data = {} + self.previous_values = {} self.username = username self.password = password @@ -254,9 +255,61 @@ class GrowattData: **mix_detail, **dashboard_values_for_mix, } + _LOGGER.debug( + "Finished updating data for %s (%s)", + self.device_id, + self.growatt_type, + ) except json.decoder.JSONDecodeError: _LOGGER.error("Unable to fetch data from Growatt server") - def get_data(self, variable): + def get_currency(self): + """Get the currency.""" + return self.data.get("currency") + + def get_data(self, entity_description): """Get the data.""" - return self.data.get(variable) + _LOGGER.debug( + "Data request for: %s", + entity_description.name, + ) + variable = entity_description.api_key + api_value = self.data.get(variable) + previous_value = self.previous_values.get(variable) + return_value = api_value + + # If we have a 'drop threshold' specified, then check it and correct if needed + if ( + entity_description.previous_value_drop_threshold is not None + and previous_value is not None + and api_value is not None + ): + _LOGGER.debug( + "%s - Drop threshold specified (%s), checking for drop... API Value: %s, Previous Value: %s", + entity_description.name, + entity_description.previous_value_drop_threshold, + api_value, + previous_value, + ) + diff = float(api_value) - float(previous_value) + + # Check if the value has dropped (negative value i.e. < 0) and it has only dropped by a + # small amount, if so, use the previous value. + # Note - The energy dashboard takes care of drops within 10% of the current value, + # however if the value is low e.g. 0.2 and drops by 0.1 it classes as a reset. + if -(entity_description.previous_value_drop_threshold) <= diff < 0: + _LOGGER.debug( + "Diff is negative, but only by a small amount therefore not a nightly reset, " + "using previous value (%s) instead of api value (%s)", + previous_value, + api_value, + ) + return_value = previous_value + else: + _LOGGER.debug( + "%s - No drop detected, using API value", entity_description.name + ) + + self.previous_values[variable] = return_value + + return return_value diff --git a/homeassistant/components/growatt_server/sensor_types/mix.py b/homeassistant/components/growatt_server/sensor_types/mix.py index 6cb61ea2e08..75d816fdf60 100644 --- a/homeassistant/components/growatt_server/sensor_types/mix.py +++ b/homeassistant/components/growatt_server/sensor_types/mix.py @@ -241,5 +241,6 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + previous_value_drop_threshold=0.2, ), ) diff --git a/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py b/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py index 04822fca35b..08a20209098 100644 --- a/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py +++ b/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py @@ -19,3 +19,4 @@ class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKey precision: int | None = None currency: bool = False + previous_value_drop_threshold: float | None = None From 6371cb4ebd4ad38e401c08846031959d987b0cc0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Nov 2022 15:43:18 +0100 Subject: [PATCH 39/41] Bumped version to 2022.11.3 --- 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 f3b51bd5d3a..7b4230c79f1 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 = "2" +PATCH_VERSION: Final = "3" __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 5058c46641f..d60943ab098 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.2" +version = "2022.11.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b0714e32b1218c50f41fa64354e60e00d67561fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Nov 2022 09:37:00 -0600 Subject: [PATCH 40/41] Fix static version in homekit tests (#82201) --- tests/components/homekit/test_diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 30fe5f2d8fc..48b1b84580e 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -379,7 +379,7 @@ async def test_config_entry_with_trigger_accessory( "iid": 7, "perms": ["pr"], "type": "52", - "value": "2022.12.0", + "value": ANY, }, ], "iid": 1, From 74c2639495d461cba48ab0f649c2cef557dc43b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Nov 2022 12:02:09 -0600 Subject: [PATCH 41/41] Fix missing await in nexia emergency heat (#82207) fixes undefined --- homeassistant/components/nexia/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 66c325d2fc3..f8c08b4efd6 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -378,7 +378,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): async def async_turn_aux_heat_on(self) -> None: """Turn Aux Heat on.""" - self._thermostat.set_emergency_heat(True) + await self._thermostat.set_emergency_heat(True) self._signal_thermostat_update() async def async_turn_off(self) -> None: