From e09605ff9bf9bcb20e590702a6e8bc527800519c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Sep 2022 20:49:01 +0200 Subject: [PATCH 001/183] Bumped version to 2022.10.0b0 --- 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 27d172265ce..32e54e4fe24 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __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 c76b015c84a..0fb94710112 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0.dev0" +version = "2022.10.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b15af5beca3d6ec3f3cffc3c67e5d1361036d876 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Thu, 29 Sep 2022 11:26:28 -0600 Subject: [PATCH 002/183] Resolve traceback error when using variables in template triggers (#77287) Co-authored-by: Erik --- homeassistant/helpers/trigger.py | 63 ++++++++++++++++++++++++------- tests/helpers/test_trigger.py | 64 +++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 9fde56ec7aa..4cb724a6435 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -2,10 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine import functools import logging -from typing import TYPE_CHECKING, Any, Protocol, TypedDict +from typing import TYPE_CHECKING, Any, Protocol, TypedDict, cast import voluptuous as vol @@ -16,7 +16,13 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_VARIABLES, ) -from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Context, + HomeAssistant, + callback, + is_callback, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -101,20 +107,51 @@ async def async_validate_trigger_config( def _trigger_action_wrapper( hass: HomeAssistant, action: Callable, conf: ConfigType ) -> Callable: - """Wrap trigger action with extra vars if configured.""" + """Wrap trigger action with extra vars if configured. + + If action is a coroutine function, a coroutine function will be returned. + If action is a callback, a callback will be returned. + """ if CONF_VARIABLES not in conf: return action - @functools.wraps(action) - async def with_vars( - run_variables: dict[str, Any], context: Context | None = None - ) -> None: - """Wrap action with extra vars.""" - trigger_variables = conf[CONF_VARIABLES] - run_variables.update(trigger_variables.async_render(hass, run_variables)) - await action(run_variables, context) + # Check for partials to properly determine if coroutine function + check_func = action + while isinstance(check_func, functools.partial): + check_func = check_func.func - return with_vars + wrapper_func: Callable[..., None] | Callable[..., Coroutine[Any, Any, None]] + if asyncio.iscoroutinefunction(check_func): + async_action = cast(Callable[..., Coroutine[Any, Any, None]], action) + + @functools.wraps(async_action) + async def async_with_vars( + run_variables: dict[str, Any], context: Context | None = None + ) -> None: + """Wrap action with extra vars.""" + trigger_variables = conf[CONF_VARIABLES] + run_variables.update(trigger_variables.async_render(hass, run_variables)) + await action(run_variables, context) + + wrapper_func = async_with_vars + + else: + + @functools.wraps(action) + async def with_vars( + run_variables: dict[str, Any], context: Context | None = None + ) -> None: + """Wrap action with extra vars.""" + trigger_variables = conf[CONF_VARIABLES] + run_variables.update(trigger_variables.async_render(hass, run_variables)) + action(run_variables, context) + + if is_callback(check_func): + with_vars = callback(with_vars) + + wrapper_func = with_vars + + return wrapper_func async def async_initialize_triggers( diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 7cee307f3ec..9cd3b0956ce 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -1,12 +1,13 @@ """The tests for the trigger helper.""" -from unittest.mock import MagicMock, call, patch +from unittest.mock import ANY, MagicMock, call, patch import pytest import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.trigger import ( _async_get_trigger_platform, + async_initialize_triggers, async_validate_trigger_config, ) from homeassistant.setup import async_setup_component @@ -137,3 +138,62 @@ async def test_trigger_alias( "Automation trigger 'My event' triggered by event 'trigger_event'" in caplog.text ) + + +async def test_async_initialize_triggers( + hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture +) -> None: + """Test async_initialize_triggers with different action types.""" + + log_cb = MagicMock() + + action_calls = [] + + trigger_config = await async_validate_trigger_config( + hass, + [ + { + "platform": "event", + "event_type": ["trigger_event"], + "variables": { + "name": "Paulus", + "via_event": "{{ trigger.event.event_type }}", + }, + } + ], + ) + + async def async_action(*args): + action_calls.append([*args]) + + @callback + def cb_action(*args): + action_calls.append([*args]) + + def non_cb_action(*args): + action_calls.append([*args]) + + for action in (async_action, cb_action, non_cb_action): + action_calls = [] + + unsub = await async_initialize_triggers( + hass, + trigger_config, + action, + "test", + "", + log_cb, + ) + await hass.async_block_till_done() + + hass.bus.async_fire("trigger_event") + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(action_calls) == 1 + assert action_calls[0][0]["name"] == "Paulus" + assert action_calls[0][0]["via_event"] == "trigger_event" + log_cb.assert_called_once_with(ANY, "Initialized trigger") + + log_cb.reset_mock() + unsub() From e421b90838380ef23e0f25b10d5c1a2f309d5c82 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 29 Sep 2022 13:40:53 +0300 Subject: [PATCH 003/183] Allow entries with same user_key for Pushover (#77904) * Allow entries with same user_key for Pushover * remove unique_id completely * Abort reauth if entered api_key already exists Update tests --- homeassistant/components/pushover/__init__.py | 4 ++ .../components/pushover/config_flow.py | 14 ++++- tests/components/pushover/test_config_flow.py | 51 ++++++++++++++++--- tests/components/pushover/test_init.py | 10 ++++ 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pushover/__init__.py b/homeassistant/components/pushover/__init__.py index fa9a9c5ebd9..3c0c92db044 100644 --- a/homeassistant/components/pushover/__init__.py +++ b/homeassistant/components/pushover/__init__.py @@ -25,6 +25,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up pushover from a config entry.""" + # remove unique_id for beta users + if entry.unique_id is not None: + hass.config_entries.async_update_entry(entry, unique_id=None) + pushover_api = PushoverAPI(entry.data[CONF_API_KEY]) try: await hass.async_add_executor_job( diff --git a/homeassistant/components/pushover/config_flow.py b/homeassistant/components/pushover/config_flow.py index 3f12446733e..ddb61d4bbc3 100644 --- a/homeassistant/components/pushover/config_flow.py +++ b/homeassistant/components/pushover/config_flow.py @@ -62,6 +62,12 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None and self._reauth_entry: user_input = {**self._reauth_entry.data, **user_input} + self._async_abort_entries_match( + { + CONF_USER_KEY: user_input[CONF_USER_KEY], + CONF_API_KEY: user_input[CONF_API_KEY], + } + ) errors = await validate_input(self.hass, user_input) if not errors: self.hass.config_entries.async_update_entry( @@ -87,9 +93,13 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_USER_KEY]) - self._abort_if_unique_id_configured() + self._async_abort_entries_match( + { + CONF_USER_KEY: user_input[CONF_USER_KEY], + CONF_API_KEY: user_input[CONF_API_KEY], + } + ) self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) errors = await validate_input(self.hass, user_input) diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py index 21c0bc3ab1e..1e919167c6a 100644 --- a/tests/components/pushover/test_config_flow.py +++ b/tests/components/pushover/test_config_flow.py @@ -48,12 +48,11 @@ async def test_flow_user(hass: HomeAssistant) -> None: assert result["data"] == MOCK_CONFIG -async def test_flow_user_key_already_configured(hass: HomeAssistant) -> None: - """Test user initialized flow with duplicate user key.""" +async def test_flow_user_key_api_key_exists(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate user key / api key pair.""" entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, - unique_id="MYUSERKEY", ) entry.add_to_hass(hass) @@ -171,7 +170,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == "form" + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -181,7 +180,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -213,7 +212,47 @@ async def test_reauth_failed(hass: HomeAssistant, mock_pushover: MagicMock) -> N }, ) - assert result2["type"] == "form" + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == { CONF_API_KEY: "invalid_api_key", } + + +async def test_reauth_with_existing_config(hass: HomeAssistant) -> None: + """Test reauth fails if the api key entered exists in another entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + second_entry = MOCK_CONFIG.copy() + second_entry[CONF_API_KEY] = "MYAPIKEY2" + + entry2 = MockConfigEntry( + domain=DOMAIN, + data=second_entry, + ) + entry2.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "MYAPIKEY2", + }, + ) + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py index 4d1ee3cae19..7a8b02c93a0 100644 --- a/tests/components/pushover/test_init.py +++ b/tests/components/pushover/test_init.py @@ -68,6 +68,16 @@ async def test_async_setup_entry_success(hass: HomeAssistant) -> None: assert entry.state == ConfigEntryState.LOADED +async def test_unique_id_updated(hass: HomeAssistant) -> None: + """Test updating unique_id to new format.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, unique_id="MYUSERKEY") + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + assert entry.unique_id is None + + async def test_async_setup_entry_failed_invalid_api_key( hass: HomeAssistant, mock_pushover: MagicMock ) -> None: From db7fabc53f352de0b458afdfedd8707f30565bdd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 28 Sep 2022 21:24:04 -0400 Subject: [PATCH 004/183] Bump zwave-js-server-python to 0.42.0 (#79020) --- homeassistant/components/zwave_js/__init__.py | 7 +- homeassistant/components/zwave_js/api.py | 4 +- homeassistant/components/zwave_js/const.py | 4 + .../components/zwave_js/device_action.py | 4 +- .../components/zwave_js/diagnostics.py | 6 +- .../zwave_js/discovery_data_template.py | 4 +- homeassistant/components/zwave_js/entity.py | 6 +- homeassistant/components/zwave_js/helpers.py | 4 +- .../components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/services.py | 10 +- .../zwave_js/triggers/value_updated.py | 6 +- homeassistant/const.py | 1 + homeassistant/helpers/aiohttp_client.py | 6 +- homeassistant/helpers/httpx_client.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 58 ++-- tests/components/zwave_js/test_climate.py | 100 ------ tests/components/zwave_js/test_cover.py | 202 ------------ tests/components/zwave_js/test_fan.py | 63 ---- tests/components/zwave_js/test_humidifier.py | 145 --------- tests/components/zwave_js/test_init.py | 10 +- tests/components/zwave_js/test_light.py | 115 ------- tests/components/zwave_js/test_lock.py | 67 ---- tests/components/zwave_js/test_number.py | 25 -- tests/components/zwave_js/test_select.py | 39 --- tests/components/zwave_js/test_services.py | 289 ------------------ tests/components/zwave_js/test_siren.py | 24 +- tests/components/zwave_js/test_switch.py | 46 --- 29 files changed, 93 insertions(+), 1164 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index d676a40d32f..f8828e8cdd0 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -88,6 +88,7 @@ from .const import ( DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, + USER_AGENT, ZWAVE_JS_NOTIFICATION_EVENT, ZWAVE_JS_VALUE_NOTIFICATION_EVENT, ZWAVE_JS_VALUE_UPDATED_EVENT, @@ -129,7 +130,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if use_addon := entry.data.get(CONF_USE_ADDON): await async_ensure_addon_running(hass, entry) - client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) + client = ZwaveClient( + entry.data[CONF_URL], + async_get_clientsession(hass), + additional_user_agent_components=USER_AGENT, + ) # connect and throw error if connection failed try: diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 98078c8457f..7ceca062ee4 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -68,6 +68,7 @@ from .const import ( DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, + USER_AGENT, ) from .helpers import ( async_enable_statistics, @@ -466,7 +467,7 @@ async def websocket_network_status( ) return controller = driver.controller - await controller.async_get_state() + controller.update(await controller.async_get_state()) client_version_info = client.version assert client_version_info # When client is connected version info is set. data = { @@ -2064,6 +2065,7 @@ class FirmwareUploadView(HomeAssistantView): uploaded_file.filename, await hass.async_add_executor_job(uploaded_file.file.read), async_get_clientsession(hass), + additional_user_agent_components=USER_AGENT, target=target, ) except BaseZwaveJSServerError as err: diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index ff1a97d6ecc..3967709ccc8 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -1,6 +1,10 @@ """Constants for the Z-Wave JS integration.""" import logging +from homeassistant.const import APPLICATION_NAME, __version__ as HA_VERSION + +USER_AGENT = {APPLICATION_NAME: HA_VERSION} + CONF_ADDON_DEVICE = "device" CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware" CONF_ADDON_LOG_LEVEL = "log_level" diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 728b376228e..004e4cc2aae 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -9,7 +9,7 @@ import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.lock import ATTR_CODE_SLOT, ATTR_USERCODE from zwave_js_server.const.command_class.meter import CC_SPECIFIC_METER_TYPE -from zwave_js_server.model.value import get_value_id +from zwave_js_server.model.value import get_value_id_str from zwave_js_server.util.command_class.meter import get_meter_type from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN @@ -341,7 +341,7 @@ async def async_get_action_capabilities( } if action_type == SERVICE_SET_CONFIG_PARAMETER: - value_id = get_value_id( + value_id = get_value_id_str( node, CommandClass.CONFIGURATION, config[ATTR_CONFIG_PARAMETER], diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index f9a30528863..ef34a2f12de 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DATA_CLIENT, DOMAIN +from .const import DATA_CLIENT, DOMAIN, USER_AGENT from .helpers import ( get_home_and_node_id_from_device_entry, get_state_key_from_unique_id, @@ -138,7 +138,9 @@ async def async_get_config_entry_diagnostics( ) -> list[dict]: """Return diagnostics for a config entry.""" msgs: list[dict] = async_redact_data( - await dump_msgs(config_entry.data[CONF_URL], async_get_clientsession(hass)), + await dump_msgs( + config_entry.data[CONF_URL], async_get_clientsession(hass), USER_AGENT + ), KEYS_TO_REDACT, ) handshake_msgs = msgs[:-1] diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 74847c3f4da..9ae1cd36d13 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -80,7 +80,7 @@ from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ( ConfigurationValue as ZwaveConfigurationValue, Value as ZwaveValue, - get_value_id, + get_value_id_str, ) from zwave_js_server.util.command_class.meter import get_meter_scale_type from zwave_js_server.util.command_class.multilevel_sensor import ( @@ -263,7 +263,7 @@ class BaseDiscoverySchemaDataTemplate: node: ZwaveNode, value_id_obj: ZwaveValueID ) -> ZwaveValue | ZwaveConfigurationValue | None: """Get a ZwaveValue from a node using a ZwaveValueDict.""" - value_id = get_value_id( + value_id = get_value_id_str( node, value_id_obj.command_class, value_id_obj.property_, diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 621316a166f..53946a07982 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -3,7 +3,7 @@ from __future__ import annotations from zwave_js_server.const import NodeStatus from zwave_js_server.model.driver import Driver -from zwave_js_server.model.value import Value as ZwaveValue, get_value_id +from zwave_js_server.model.value import Value as ZwaveValue, get_value_id_str from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback @@ -242,7 +242,7 @@ class ZWaveBaseEntity(Entity): endpoint = self.info.primary_value.endpoint # lookup value by value_id - value_id = get_value_id( + value_id = get_value_id_str( self.info.node, command_class, value_property, @@ -256,7 +256,7 @@ class ZWaveBaseEntity(Entity): if return_value is None and check_all_endpoints: for endpoint_idx in self.info.node.endpoints: if endpoint_idx != self.info.primary_value.endpoint: - value_id = get_value_id( + value_id = get_value_id_str( self.info.node, command_class, value_property, diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 6175b7db353..6949f3654a5 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -14,7 +14,7 @@ from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ( ConfigurationValue, Value as ZwaveValue, - get_value_id, + get_value_id_str, ) from homeassistant.components.group import expand_entity_ids @@ -317,7 +317,7 @@ def get_zwave_value_from_config(node: ZwaveNode, config: ConfigType) -> ZwaveVal property_key = None if config.get(ATTR_PROPERTY_KEY): property_key = config[ATTR_PROPERTY_KEY] - value_id = get_value_id( + value_id = get_value_id_str( node, config[ATTR_COMMAND_CLASS], config[ATTR_PROPERTY], diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 7c569301831..9880d5bb5d1 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.41.1"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.42.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index d60532fcf75..63a9071ffb6 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -12,7 +12,7 @@ from zwave_js_server.const import CommandClass, CommandStatus from zwave_js_server.exceptions import SetValueFailed from zwave_js_server.model.endpoint import Endpoint from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.value import ValueDataType, get_value_id +from zwave_js_server.model.value import ValueDataType, get_value_id_str from zwave_js_server.util.multicast import async_multicast_set_value from zwave_js_server.util.node import ( async_bulk_set_partial_config_parameters, @@ -497,7 +497,7 @@ class ZWaveServices: coros = [] for node in nodes: - value_id = get_value_id( + value_id = get_value_id_str( node, command_class, property_, @@ -582,13 +582,15 @@ class ZWaveServices: first_node = next( node for node in client.driver.controller.nodes.values() - if get_value_id(node, command_class, property_, endpoint, property_key) + if get_value_id_str( + node, command_class, property_, endpoint, property_key + ) in node.values ) # If value has a string type but the new value is not a string, we need to # convert it to one - value_id = get_value_id( + value_id = get_value_id_str( first_node, command_class, property_, endpoint, property_key ) if ( diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 780d1251911..655d1f9070e 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -5,7 +5,7 @@ import functools import voluptuous as vol from zwave_js_server.const import CommandClass -from zwave_js_server.model.value import Value, get_value_id +from zwave_js_server.model.value import Value, get_value_id_str from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback @@ -164,7 +164,9 @@ async def async_attach_trigger( device_identifier = get_device_id(driver, node) device = dev_reg.async_get_device({device_identifier}) assert device - value_id = get_value_id(node, command_class, property_, endpoint, property_key) + value_id = get_value_id_str( + node, command_class, property_, endpoint, property_key + ) value = node.values[value_id] # We need to store the current value and device for the callback unsubs.append( diff --git a/homeassistant/const.py b/homeassistant/const.py index 32e54e4fe24..22f66ed452b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,6 +5,7 @@ from typing import Final from .backports.enum import StrEnum +APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 10 PATCH_VERSION: Final = "0b0" diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index f44b59ff077..2558b5d0896 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -16,7 +16,7 @@ from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout from homeassistant import config_entries -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util @@ -32,8 +32,8 @@ DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" DATA_CLIENTSESSION = "aiohttp_clientsession" DATA_CLIENTSESSION_NOTVERIFY = "aiohttp_clientsession_notverify" -SERVER_SOFTWARE = "HomeAssistant/{0} aiohttp/{1} Python/{2[0]}.{2[1]}".format( - __version__, aiohttp.__version__, sys.version_info +SERVER_SOFTWARE = "{0}/{1} aiohttp/{2} Python/{3[0]}.{3[1]}".format( + APPLICATION_NAME, __version__, aiohttp.__version__, sys.version_info ) WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session" diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index e2ebbd31dac..b932a1cd795 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -7,7 +7,7 @@ from typing import Any import httpx -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.loader import bind_hass @@ -15,8 +15,8 @@ from .frame import warn_use DATA_ASYNC_CLIENT = "httpx_async_client" DATA_ASYNC_CLIENT_NOVERIFY = "httpx_async_client_noverify" -SERVER_SOFTWARE = "HomeAssistant/{0} httpx/{1} Python/{2[0]}.{2[1]}".format( - __version__, httpx.__version__, sys.version_info +SERVER_SOFTWARE = "{0}/{1} httpx/{2} Python/{3[0]}.{3[1]}".format( + APPLICATION_NAME, __version__, httpx.__version__, sys.version_info ) USER_AGENT = "User-Agent" diff --git a/requirements_all.txt b/requirements_all.txt index 8892c8a5b18..aeade9111f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2619,7 +2619,7 @@ zigpy==0.50.3 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.1 +zwave-js-server-python==0.42.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c97e09fd37..fd4499b9f6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1811,7 +1811,7 @@ zigpy-znp==0.8.2 zigpy==0.50.3 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.1 +zwave-js-server-python==0.42.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.6 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index ed7c23aef79..b55f4941a49 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -86,13 +86,18 @@ def get_device(hass, node): return dev_reg.async_get_device({device_id}) -async def test_network_status(hass, multisensor_6, integration, hass_ws_client): +async def test_network_status( + hass, multisensor_6, controller_state, integration, hass_ws_client +): """Test the network status websocket command.""" entry = integration ws_client = await hass_ws_client(hass) # Try API call with entry ID - with patch("zwave_js_server.model.controller.Controller.async_get_state"): + with patch( + "zwave_js_server.model.controller.Controller.async_get_state", + return_value=controller_state["controller"], + ): await ws_client.send_json( { ID: 1, @@ -113,7 +118,10 @@ async def test_network_status(hass, multisensor_6, integration, hass_ws_client): identifiers={(DOMAIN, "3245146787-52")}, ) assert device - with patch("zwave_js_server.model.controller.Controller.async_get_state"): + with patch( + "zwave_js_server.model.controller.Controller.async_get_state", + return_value=controller_state["controller"], + ): await ws_client.send_json( { ID: 2, @@ -2585,27 +2593,10 @@ async def test_set_config_parameter( assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -2633,27 +2624,10 @@ async def test_set_config_parameter( assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -2842,13 +2816,19 @@ async def test_firmware_upload_view( device = get_device(hass, multisensor_6) with patch( "homeassistant.components.zwave_js.api.begin_firmware_update", - ) as mock_cmd: + ) as mock_cmd, patch.dict( + "homeassistant.components.zwave_js.api.USER_AGENT", + {"HomeAssistant": "0.0.0"}, + ): resp = await client.post( f"/api/zwave_js/firmware/upload/{device.id}", data={"file": firmware_file, "target": "15"}, ) assert mock_cmd.call_args[0][1:4] == (multisensor_6, "file", bytes(10)) - assert mock_cmd.call_args[1] == {"target": 15} + assert mock_cmd.call_args[1] == { + "target": 15, + "additional_user_agent_components": {"HomeAssistant": "0.0.0"}, + } assert json.loads(await resp.text()) is None diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index e0b8dbe569f..4b4519c07b9 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -86,21 +86,9 @@ async def test_thermostat_v2( assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Mode", "commandClass": 64, "endpoint": 1, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 31, - "label": "Thermostat mode", - "states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"}, - }, - "value": 1, } assert args["value"] == 2 @@ -123,42 +111,19 @@ async def test_thermostat_v2( assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Mode", "commandClass": 64, "endpoint": 1, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 31, - "label": "Thermostat mode", - "states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"}, - }, - "value": 1, } assert args["value"] == 2 args = client.async_send_command.call_args_list[1][0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Setpoint", "commandClass": 67, "endpoint": 1, "property": "setpoint", "propertyKey": 1, - "propertyName": "setpoint", - "propertyKeyName": "Heating", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "°F", - "ccSpecific": {"setpointType": 1}, - }, - "value": 72, } assert args["value"] == 77 @@ -232,21 +197,10 @@ async def test_thermostat_v2( assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Setpoint", "commandClass": 67, "endpoint": 1, "property": "setpoint", "propertyKey": 1, - "propertyName": "setpoint", - "propertyKeyName": "Heating", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "°F", - "ccSpecific": {"setpointType": 1}, - }, - "value": 72, } assert args["value"] == 77 @@ -254,21 +208,10 @@ async def test_thermostat_v2( assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Setpoint", "commandClass": 67, "endpoint": 1, "property": "setpoint", "propertyKey": 2, - "propertyName": "setpoint", - "propertyKeyName": "Cooling", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "°F", - "ccSpecific": {"setpointType": 2}, - }, - "value": 73, } assert args["value"] == 86 @@ -306,20 +249,7 @@ async def test_thermostat_v2( assert args["valueId"] == { "endpoint": 1, "commandClass": 68, - "commandClassName": "Thermostat Fan Mode", "property": "mode", - "propertyName": "mode", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "states": {"0": "Auto low", "1": "Low"}, - "label": "Thermostat fan mode", - }, - "value": 0, } assert args["value"] == 1 @@ -408,20 +338,8 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat assert args["valueId"] == { "endpoint": 0, "commandClass": 67, - "commandClassName": "Thermostat Setpoint", "property": "setpoint", - "propertyName": "setpoint", "propertyKey": 1, - "propertyKeyName": "Heating", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "\u00b0C", - "ccSpecific": {"setpointType": 1}, - }, - "value": 14, } assert args["value"] == 21.5 @@ -627,27 +545,9 @@ async def test_preset_and_no_setpoint( assert args["command"] == "node.set_value" assert args["nodeId"] == 8 assert args["valueId"] == { - "commandClassName": "Thermostat Mode", "commandClass": 64, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "ccVersion": 3, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 31, - "label": "Thermostat mode", - "states": { - "0": "Off", - "1": "Heat", - "11": "Energy heat", - "15": "Full power", - }, - }, - "value": 1, } assert args["value"] == 15 diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 0958e259ab0..54f71fa00d3 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -50,20 +50,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] == 50 @@ -82,20 +71,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] == 0 @@ -114,20 +92,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] @@ -145,18 +112,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 assert open_args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Open", - "propertyName": "Open", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Open)", - "ccSpecific": {"switchType": 3}, - }, } assert not open_args["value"] @@ -164,18 +122,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 6 assert close_args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Close", - "propertyName": "Close", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Close)", - "ccSpecific": {"switchType": 3}, - }, } assert not close_args["value"] @@ -215,20 +164,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] == 0 @@ -247,18 +185,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 assert open_args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Open", - "propertyName": "Open", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Open)", - "ccSpecific": {"switchType": 3}, - }, } assert not open_args["value"] @@ -266,18 +195,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 6 assert close_args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Close", - "propertyName": "Close", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Close)", - "ccSpecific": {"switchType": 3}, - }, } assert not close_args["value"] @@ -332,21 +252,8 @@ async def test_fibaro_FGR222_shutter_cover( assert args["valueId"] == { "endpoint": 0, "commandClass": 145, - "commandClassName": "Manufacturer Proprietary", "property": "fibaro", "propertyKey": "venetianBlindsTilt", - "propertyName": "fibaro", - "propertyKeyName": "venetianBlindsTilt", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Venetian blinds tilt", - "min": 0, - "max": 99, - }, - "value": 0, } assert args["value"] == 99 @@ -366,21 +273,8 @@ async def test_fibaro_FGR222_shutter_cover( assert args["valueId"] == { "endpoint": 0, "commandClass": 145, - "commandClassName": "Manufacturer Proprietary", "property": "fibaro", "propertyKey": "venetianBlindsTilt", - "propertyName": "fibaro", - "propertyKeyName": "venetianBlindsTilt", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Venetian blinds tilt", - "min": 0, - "max": 99, - }, - "value": 0, } assert args["value"] == 0 @@ -411,23 +305,9 @@ async def test_aeotec_nano_shutter_cover( assert args["command"] == "node.set_value" assert args["nodeId"] == 3 assert args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "value": 0, - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "valueChangeOptions": ["transitionDuration"], - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] @@ -445,20 +325,9 @@ async def test_aeotec_nano_shutter_cover( assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 3 assert open_args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "On", - "propertyName": "On", - "value": False, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (On)", - "ccSpecific": {"switchType": 1}, - }, } assert not open_args["value"] @@ -466,20 +335,9 @@ async def test_aeotec_nano_shutter_cover( assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 3 assert close_args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Off", - "propertyName": "Off", - "value": False, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Off)", - "ccSpecific": {"switchType": 1}, - }, } assert not close_args["value"] @@ -520,23 +378,9 @@ async def test_aeotec_nano_shutter_cover( assert args["command"] == "node.set_value" assert args["nodeId"] == 3 assert args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "value": 0, - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - "label": "Target value", - }, } assert args["value"] == 0 @@ -555,20 +399,9 @@ async def test_aeotec_nano_shutter_cover( assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 3 assert open_args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "On", - "propertyName": "On", - "value": False, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (On)", - "ccSpecific": {"switchType": 1}, - }, } assert not open_args["value"] @@ -576,20 +409,9 @@ async def test_aeotec_nano_shutter_cover( assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 3 assert close_args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Off", - "propertyName": "Off", - "value": False, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Off)", - "ccSpecific": {"switchType": 1}, - }, } assert not close_args["value"] @@ -631,21 +453,9 @@ async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): assert args["nodeId"] == 12 assert args["value"] == 255 assert args["valueId"] == { - "ccVersion": 0, "commandClass": 102, - "commandClassName": "Barrier Operator", "endpoint": 0, - "metadata": { - "label": "Target Barrier State", - "max": 255, - "min": 0, - "readable": True, - "states": {"0": "Closed", "255": "Open"}, - "type": "number", - "writeable": True, - }, "property": "targetState", - "propertyName": "targetState", } # state doesn't change until currentState value update is received @@ -665,21 +475,9 @@ async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): assert args["nodeId"] == 12 assert args["value"] == 0 assert args["valueId"] == { - "ccVersion": 0, "commandClass": 102, - "commandClassName": "Barrier Operator", "endpoint": 0, - "metadata": { - "label": "Target Barrier State", - "max": 255, - "min": 0, - "readable": True, - "states": {"0": "Closed", "255": "Open"}, - "type": "number", - "writeable": True, - }, "property": "targetState", - "propertyName": "targetState", } # state doesn't change until currentState value update is received diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 97b4a0074bc..2e200d9e4e9 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -54,19 +54,9 @@ async def test_generic_fan(hass, client, fan_generic, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 17 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - }, } assert args["value"] == 66 @@ -96,19 +86,9 @@ async def test_generic_fan(hass, client, fan_generic, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 17 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - }, } assert args["value"] == 255 @@ -127,19 +107,9 @@ async def test_generic_fan(hass, client, fan_generic, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 17 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - }, } assert args["value"] == 0 @@ -602,22 +572,9 @@ async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 3, - "commandClassName": "Thermostat Fan Mode", "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "label": "Thermostat fan mode", - "max": 255, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "states": {"0": "Auto low", "1": "Low", "6": "Circulation"}, - }, - "value": 0, } assert args["value"] == 1 @@ -647,19 +604,9 @@ async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 3, - "commandClassName": "Thermostat Fan Mode", "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, "endpoint": 0, "property": "off", - "propertyName": "off", - "metadata": { - "label": "Thermostat fan turned off", - "type": "boolean", - "readable": True, - "writeable": True, - }, - "value": False, } assert args["value"] @@ -678,19 +625,9 @@ async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 3, - "commandClassName": "Thermostat Fan Mode", "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, "endpoint": 0, "property": "off", - "propertyName": "off", - "metadata": { - "label": "Thermostat fan turned off", - "type": "boolean", - "readable": True, - "writeable": True, - }, - "value": False, } assert not args["value"] diff --git a/tests/components/zwave_js/test_humidifier.py b/tests/components/zwave_js/test_humidifier.py index 37280ff5ad4..7952f639fe6 100644 --- a/tests/components/zwave_js/test_humidifier.py +++ b/tests/components/zwave_js/test_humidifier.py @@ -56,24 +56,10 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 1, - "commandClassName": "Humidity Control Setpoint", "commandClass": CommandClass.HUMIDITY_CONTROL_SETPOINT, "endpoint": 0, "property": "setpoint", "propertyKey": 1, - "propertyName": "setpoint", - "propertyKeyName": "Humidifier", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "%", - "min": 10, - "max": 70, - "ccSpecific": {"setpointType": 1}, - }, - "value": 35, } assert args["value"] == 41 @@ -186,22 +172,9 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.HUMIDIFY), } assert args["value"] == int(HumidityControlMode.OFF) @@ -239,22 +212,9 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.AUTO), } assert args["value"] == int(HumidityControlMode.DEHUMIDIFY) @@ -416,22 +376,9 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.DEHUMIDIFY), } assert args["value"] == int(HumidityControlMode.AUTO) @@ -469,22 +416,9 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.OFF), } assert args["value"] == int(HumidityControlMode.HUMIDIFY) @@ -570,22 +504,9 @@ async def test_humidifier_missing_mode( assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.AUTO), } assert args["value"] == int(HumidityControlMode.OFF) @@ -623,24 +544,10 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 1, - "commandClassName": "Humidity Control Setpoint", "commandClass": CommandClass.HUMIDITY_CONTROL_SETPOINT, "endpoint": 0, "property": "setpoint", "propertyKey": 2, - "propertyName": "setpoint", - "propertyKeyName": "De-humidifier", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "%", - "min": 30, - "max": 90, - "ccSpecific": {"setpointType": 2}, - }, - "value": 60, } assert args["value"] == 41 @@ -753,22 +660,9 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.DEHUMIDIFY), } assert args["value"] == int(HumidityControlMode.OFF) @@ -806,22 +700,9 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.AUTO), } assert args["value"] == int(HumidityControlMode.HUMIDIFY) @@ -983,22 +864,9 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.HUMIDIFY), } assert args["value"] == int(HumidityControlMode.AUTO) @@ -1036,21 +904,8 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.OFF), } assert args["value"] == int(HumidityControlMode.DEHUMIDIFY) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index c69c5a09f89..4f58c87febb 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -8,6 +8,7 @@ from zwave_js_server.client import Client from zwave_js_server.event import Event from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.node import Node +from zwave_js_server.model.version import VersionInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js import DOMAIN @@ -667,6 +668,7 @@ async def test_update_addon( backup_calls, update_addon_side_effect, create_backup_side_effect, + version_state, ): """Test update the Z-Wave JS add-on during entry setup.""" device = "/test" @@ -677,7 +679,9 @@ async def test_update_addon( addon_info.return_value["update_available"] = update_available create_backup.side_effect = create_backup_side_effect update_addon.side_effect = update_addon_side_effect - client.connect.side_effect = InvalidServerVersion("Invalid version") + client.connect.side_effect = InvalidServerVersion( + VersionInfo("a", "b", 1, 1, 1), 1, "Invalid version" + ) entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", @@ -703,7 +707,9 @@ async def test_issue_registry(hass, client, version_state): device = "/test" network_key = "abc123" - client.connect.side_effect = InvalidServerVersion("Invalid version") + client.connect.side_effect = InvalidServerVersion( + VersionInfo("a", "b", 1, 1, 1), 1, "Invalid version" + ) entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index d2434f01963..cd7d9e91459 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -60,20 +60,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 255 @@ -92,20 +81,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 255 assert args["options"]["transitionDuration"] == "10s" @@ -164,20 +142,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 50 assert args["options"]["transitionDuration"] == "default" @@ -201,20 +168,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 50 assert args["options"]["transitionDuration"] == "20s" @@ -233,12 +189,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 39 - assert args["valueId"]["commandClassName"] == "Color Switch" assert args["valueId"]["commandClass"] == 51 assert args["valueId"]["endpoint"] == 0 - assert args["valueId"]["metadata"]["label"] == "Target Color" assert args["valueId"]["property"] == "targetColor" - assert args["valueId"]["propertyName"] == "targetColor" assert args["value"] == { "blue": 255, "coldWhite": 0, @@ -332,12 +285,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): args = client.async_send_command.call_args_list[0][0][0] # red 0 assert args["command"] == "node.set_value" assert args["nodeId"] == 39 - assert args["valueId"]["commandClassName"] == "Color Switch" assert args["valueId"]["commandClass"] == 51 assert args["valueId"]["endpoint"] == 0 - assert args["valueId"]["metadata"]["label"] == "Target Color" assert args["valueId"]["property"] == "targetColor" - assert args["valueId"]["propertyName"] == "targetColor" assert args["value"] == { "blue": 0, "coldWhite": 235, @@ -438,20 +388,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 0 @@ -493,20 +432,9 @@ async def test_rgbw_light(hass, client, zen_31, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 94 assert args["valueId"] == { - "commandClassName": "Color Switch", "commandClass": 51, "endpoint": 1, "property": "targetColor", - "propertyName": "targetColor", - "ccVersion": 0, - "metadata": { - "label": "Target Color", - "type": "any", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, - "value": {"blue": 70, "green": 159, "red": 255, "warmWhite": 141}, } assert args["value"] == {"blue": 0, "green": 0, "red": 0, "warmWhite": 128} @@ -514,22 +442,9 @@ async def test_rgbw_light(hass, client, zen_31, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 94 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 1, "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 0, - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, - "value": 59, } assert args["value"] == 255 @@ -565,19 +480,9 @@ async def test_black_is_off(hass, client, express_controls_ezmultipli, integrati assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id assert args["valueId"] == { - "commandClassName": "Color Switch", "commandClass": 51, "endpoint": 0, "property": "targetColor", - "propertyName": "targetColor", - "ccVersion": 1, - "metadata": { - "label": "Target Color", - "type": "any", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == {"red": 255, "green": 255, "blue": 255} @@ -656,19 +561,9 @@ async def test_black_is_off(hass, client, express_controls_ezmultipli, integrati assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id assert args["valueId"] == { - "commandClassName": "Color Switch", "commandClass": 51, "endpoint": 0, "property": "targetColor", - "propertyName": "targetColor", - "ccVersion": 1, - "metadata": { - "label": "Target Color", - "type": "any", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == {"red": 0, "green": 0, "blue": 0} @@ -686,19 +581,9 @@ async def test_black_is_off(hass, client, express_controls_ezmultipli, integrati assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id assert args["valueId"] == { - "commandClassName": "Color Switch", "commandClass": 51, "endpoint": 0, "property": "targetColor", - "propertyName": "targetColor", - "ccVersion": 1, - "metadata": { - "label": "Target Color", - "type": "any", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == {"red": 0, "green": 255, "blue": 0} diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 2bf4cff8b5b..5f35a568f37 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -44,29 +44,9 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { - "commandClassName": "Door Lock", "commandClass": 98, "endpoint": 0, "property": "targetMode", - "propertyName": "targetMode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Target lock mode", - "states": { - "0": "Unsecured", - "1": "UnsecuredWithTimeout", - "16": "InsideUnsecured", - "17": "InsideUnsecuredWithTimeout", - "32": "OutsideUnsecured", - "33": "OutsideUnsecuredWithTimeout", - "254": "Unknown", - "255": "Secured", - }, - }, } assert args["value"] == 255 @@ -109,29 +89,9 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { - "commandClassName": "Door Lock", "commandClass": 98, "endpoint": 0, "property": "targetMode", - "propertyName": "targetMode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Target lock mode", - "states": { - "0": "Unsecured", - "1": "UnsecuredWithTimeout", - "16": "InsideUnsecured", - "17": "InsideUnsecuredWithTimeout", - "32": "OutsideUnsecured", - "33": "OutsideUnsecuredWithTimeout", - "254": "Unknown", - "255": "Secured", - }, - }, } assert args["value"] == 0 @@ -154,22 +114,10 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { - "commandClassName": "User Code", "commandClass": 99, "endpoint": 0, "property": "userCode", - "propertyName": "userCode", "propertyKey": 1, - "propertyKeyName": "1", - "metadata": { - "type": "string", - "readable": True, - "writeable": True, - "minLength": 4, - "maxLength": 10, - "label": "User Code (1)", - }, - "value": "**********", } assert args["value"] == "1234" @@ -188,25 +136,10 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { - "commandClassName": "User Code", "commandClass": 99, "endpoint": 0, "property": "userIdStatus", - "propertyName": "userIdStatus", "propertyKey": 1, - "propertyKeyName": "1", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "User ID status (1)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled", - }, - }, - "value": 1, } assert args["value"] == 0 diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index e987bfbebc6..e36bd081b18 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -31,21 +31,9 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 4 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, - "ccVersion": 1, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] == 30.0 @@ -101,20 +89,7 @@ async def test_volume_number(hass, client, aeotec_zw164_siren, integration): assert args["valueId"] == { "endpoint": 2, "commandClass": 121, - "commandClassName": "Sound Switch", "property": "defaultVolume", - "propertyName": "defaultVolume", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Default volume", - "min": 0, - "max": 100, - "unit": "%", - }, - "value": 100, } assert args["value"] == 30 diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py index e5b415d1341..1cf5fb54304 100644 --- a/tests/components/zwave_js/test_select.py +++ b/tests/components/zwave_js/test_select.py @@ -81,19 +81,7 @@ async def test_default_tone_select( assert args["valueId"] == { "endpoint": 2, "commandClass": 121, - "commandClassName": "Sound Switch", "property": "defaultToneId", - "propertyName": "defaultToneId", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Default tone ID", - "min": 0, - "max": 254, - }, - "value": 17, } assert args["value"] == 30 @@ -164,22 +152,7 @@ async def test_protection_select( assert args["valueId"] == { "endpoint": 0, "commandClass": 117, - "commandClassName": "Protection", "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": { - "0": "Unprotected", - "1": "ProtectedBySequence", - "2": "NoOperationPossible", - }, - }, - "value": 0, } assert args["value"] == 1 @@ -264,19 +237,7 @@ async def test_multilevel_switch_select(hass, client, fortrezz_ssa1_siren, integ assert args["valueId"] == { "endpoint": 0, "commandClass": 38, - "commandClassName": "Multilevel Switch", "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "min": 0, - "max": 99, - }, } assert args["value"] == 33 diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index b5ef083bf64..6e425bff042 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -78,27 +78,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -122,27 +105,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -165,27 +131,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -208,27 +157,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 41, - "propertyName": "Temperature Threshold (Unit)", "propertyKey": 15, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 3, - "min": 1, - "max": 2, - "default": 1, - "format": 0, - "allowManualEntry": False, - "states": {"1": "Celsius", "2": "Fahrenheit"}, - "label": "Temperature Threshold (Unit)", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 2 @@ -254,27 +186,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 41, - "propertyName": "Temperature Threshold (Unit)", "propertyKey": 15, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 3, - "min": 1, - "max": 2, - "default": 1, - "format": 0, - "allowManualEntry": False, - "states": {"1": "Celsius", "2": "Fahrenheit"}, - "label": "Temperature Threshold (Unit)", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 2 @@ -298,27 +213,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -344,27 +242,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -431,27 +312,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -477,27 +341,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -551,32 +398,7 @@ async def test_set_config_parameter_gather( assert args["valueId"] == { "endpoint": 0, "commandClass": 112, - "commandClassName": "Configuration", "property": 1, - "propertyName": "Temperature Reporting Threshold", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "description": "Reporting threshold for changes in the ambient temperature", - "label": "Temperature Reporting Threshold", - "default": 2, - "min": 0, - "max": 4, - "states": { - "0": "Disabled", - "1": "0.5\u00b0 F", - "2": "1.0\u00b0 F", - "3": "1.5\u00b0 F", - "4": "2.0\u00b0 F", - }, - "valueSize": 1, - "format": 0, - "allowManualEntry": False, - "isFromConfig": True, - }, - "value": 1, } assert args["value"] == 1 @@ -847,26 +669,9 @@ async def test_refresh_value( assert args["command"] == "node.poll_value" assert args["nodeId"] == 26 assert args["valueId"] == { - "commandClassName": "Thermostat Mode", "commandClass": 64, "endpoint": 1, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Thermostat mode", - "states": { - "0": "Off", - "1": "Heat", - "2": "Cool", - }, - }, - "value": 2, - "ccVersion": 0, } client.async_send_command.reset_mock() @@ -950,20 +755,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -988,20 +782,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -1029,20 +812,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -1069,20 +841,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -1111,20 +872,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -1170,22 +920,10 @@ async def test_set_value_string( assert args["command"] == "node.set_value" assert args["nodeId"] == lock_schlage_be469.node_id assert args["valueId"] == { - "commandClassName": "User Code", "commandClass": 99, "endpoint": 0, "property": "userCode", - "propertyName": "userCode", "propertyKey": 1, - "propertyKeyName": "1", - "metadata": { - "type": "string", - "readable": True, - "writeable": True, - "minLength": 4, - "maxLength": 10, - "label": "User Code (1)", - }, - "value": "**********", } assert args["value"] == "12345" @@ -1212,17 +950,7 @@ async def test_set_value_options(hass, client, aeon_smart_switch_6, integration) assert args["valueId"] == { "endpoint": 0, "commandClass": 37, - "commandClassName": "Binary Switch", "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 2 assert args["options"] == {"transitionDuration": 1} @@ -1263,27 +991,10 @@ async def test_set_value_gather( assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, "propertyKey": 1, - "propertyName": "Group 2: Send battery reports", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 diff --git a/tests/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py index 3284526aa0f..75d43c545bd 100644 --- a/tests/components/zwave_js/test_siren.py +++ b/tests/components/zwave_js/test_siren.py @@ -115,7 +115,11 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == TONE_ID_VALUE_ID + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "property": "toneId", + } assert args["value"] == 255 client.async_send_command.reset_mock() @@ -159,7 +163,11 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == {**TONE_ID_VALUE_ID, "value": 255} + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "property": "toneId", + } assert args["value"] == 1 assert args["options"] == {"volume": 50} @@ -181,7 +189,11 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == {**TONE_ID_VALUE_ID, "value": 255} + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "property": "toneId", + } assert args["value"] == 1 assert args["options"] == {"volume": 50} @@ -199,7 +211,11 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == {**TONE_ID_VALUE_ID, "value": 255} + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "property": "toneId", + } assert args["value"] == 0 client.async_send_command.reset_mock() diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py index ea6e27d9b72..b84ab32f618 100644 --- a/tests/components/zwave_js/test_switch.py +++ b/tests/components/zwave_js/test_switch.py @@ -25,18 +25,9 @@ async def test_switch(hass, hank_binary_switch, integration, client): assert args["command"] == "node.set_value" assert args["nodeId"] == 32 assert args["valueId"] == { - "commandClassName": "Binary Switch", "commandClass": 37, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Target value", - }, - "value": False, } assert args["value"] is True @@ -72,18 +63,9 @@ async def test_switch(hass, hank_binary_switch, integration, client): assert args["command"] == "node.set_value" assert args["nodeId"] == 32 assert args["valueId"] == { - "commandClassName": "Binary Switch", "commandClass": 37, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Target value", - }, - "value": False, } assert args["value"] is False @@ -108,24 +90,10 @@ async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client): assert args["nodeId"] == 12 assert args["value"] == 0 assert args["valueId"] == { - "ccVersion": 0, "commandClass": 102, - "commandClassName": "Barrier Operator", "endpoint": 0, - "metadata": { - "label": "Signaling State (Visual)", - "max": 255, - "min": 0, - "readable": True, - "states": {"0": "Off", "255": "On"}, - "type": "number", - "writeable": True, - }, "property": "signalingState", "propertyKey": 2, - "propertyKeyName": "2", - "propertyName": "signalingState", - "value": 255, } # state change is optimistic and writes state @@ -149,24 +117,10 @@ async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client): assert args["nodeId"] == 12 assert args["value"] == 255 assert args["valueId"] == { - "ccVersion": 0, "commandClass": 102, - "commandClassName": "Barrier Operator", "endpoint": 0, - "metadata": { - "label": "Signaling State (Visual)", - "max": 255, - "min": 0, - "readable": True, - "states": {"0": "Off", "255": "On"}, - "type": "number", - "writeable": True, - }, "property": "signalingState", "propertyKey": 2, - "propertyKeyName": "2", - "propertyName": "signalingState", - "value": 255, } # state change is optimistic and writes state From 3e5523c54911bc5cf158c46e7a2f91864f4995cf Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Thu, 29 Sep 2022 01:39:15 +0000 Subject: [PATCH 005/183] Add to issue registry if user has mirrored entries for breaking in #67631 (#79208) Co-authored-by: Diogo Gomes --- .../components/bayesian/binary_sensor.py | 17 ++++ homeassistant/components/bayesian/repairs.py | 39 ++++++++ .../components/bayesian/translations/en.json | 8 ++ .../components/bayesian/test_binary_sensor.py | 92 +++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 homeassistant/components/bayesian/repairs.py create mode 100644 homeassistant/components/bayesian/translations/en.json diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 73ebcc8b37e..0e943b2d0ad 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -34,6 +34,7 @@ from homeassistant.helpers.template import result_as_boolean from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS +from .repairs import raise_mirrored_entries ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" @@ -245,6 +246,22 @@ class BayesianBinarySensor(BinarySensorEntity): self.probability = self._calculate_new_probability() self._attr_is_on = bool(self.probability >= self._probability_threshold) + # detect mirrored entries + for entity, observations in self.observations_by_entity.items(): + raise_mirrored_entries( + self.hass, observations, text=f"{self._attr_name}/{entity}" + ) + + all_template_observations = [] + for value in self.observations_by_template.values(): + all_template_observations.append(value[0]) + if len(all_template_observations) == 2: + raise_mirrored_entries( + self.hass, + all_template_observations, + text=f"{self._attr_name}/{all_template_observations[0]['value_template']}", + ) + @callback def _recalculate_and_write_state(self): self.probability = self._calculate_new_probability() diff --git a/homeassistant/components/bayesian/repairs.py b/homeassistant/components/bayesian/repairs.py new file mode 100644 index 00000000000..a1391f8c550 --- /dev/null +++ b/homeassistant/components/bayesian/repairs.py @@ -0,0 +1,39 @@ +"""Helpers for generating repairs.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry + +from . import DOMAIN + + +def raise_mirrored_entries(hass: HomeAssistant, observations, text: str = "") -> None: + """If there are mirrored entries, the user is probably using a workaround for a patched bug.""" + if len(observations) != 2: + return + true_sums_1: bool = ( + round( + observations[0]["prob_given_true"] + observations[1]["prob_given_true"], 1 + ) + == 1.0 + ) + false_sums_1: bool = ( + round( + observations[0]["prob_given_false"] + observations[1]["prob_given_false"], 1 + ) + == 1.0 + ) + same_states: bool = observations[0]["platform"] == observations[1]["platform"] + if true_sums_1 & false_sums_1 & same_states: + issue_registry.async_create_issue( + hass, + DOMAIN, + "mirrored_entry/" + text, + breaks_in_ha_version="2022.10.0", + is_fixable=False, + is_persistent=False, + severity=issue_registry.IssueSeverity.WARNING, + translation_key="manual_migration", + translation_placeholders={"entity": text}, + learn_more_url="https://github.com/home-assistant/core/pull/67631", + ) diff --git a/homeassistant/components/bayesian/translations/en.json b/homeassistant/components/bayesian/translations/en.json new file mode 100644 index 00000000000..ae9e5645f73 --- /dev/null +++ b/homeassistant/components/bayesian/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "manual_migration": { + "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", + "title": "Manual YAML fix required for Bayesian" + } + } +} diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 357cacb4214..0344e2b9445 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, callback from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.issue_registry import async_get from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @@ -184,6 +185,8 @@ async def test_sensor_numeric_state(hass): assert state.state == "off" + assert len(async_get(hass).issues) == 0 + async def test_sensor_state(hass): """Test sensor on state platform observations.""" @@ -341,6 +344,7 @@ async def test_threshold(hass): assert round(abs(1.0 - state.attributes.get("probability")), 7) == 0 assert state.state == "on" + assert len(async_get(hass).issues) == 0 async def test_multiple_observations(hass): @@ -495,6 +499,94 @@ async def test_multiple_numeric_observations(hass): assert state.attributes.get("observations")[1]["platform"] == "numeric_state" +async def test_mirrored_observations(hass): + """Test whether mirrored entries are detected and appropriate issues are created.""" + + config = { + "binary_sensor": { + "platform": "bayesian", + "name": "Test_Binary", + "observations": [ + { + "platform": "state", + "entity_id": "binary_sensor.test_monitored", + "to_state": "on", + "prob_given_true": 0.8, + "prob_given_false": 0.4, + }, + { + "platform": "state", + "entity_id": "binary_sensor.test_monitored", + "to_state": "off", + "prob_given_true": 0.2, + "prob_given_false": 0.59, + }, + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored1", + "above": 5, + "prob_given_true": 0.7, + "prob_given_false": 0.4, + }, + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored1", + "below": 5, + "prob_given_true": 0.3, + "prob_given_false": 0.6, + }, + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored2') == 'off'}}", + "prob_given_true": 0.79, + "prob_given_false": 0.4, + }, + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored2') == 'on'}}", + "prob_given_true": 0.2, + "prob_given_false": 0.6, + }, + { + "platform": "state", + "entity_id": "sensor.colour", + "to_state": "blue", + "prob_given_true": 0.33, + "prob_given_false": 0.8, + }, + { + "platform": "state", + "entity_id": "sensor.colour", + "to_state": "green", + "prob_given_true": 0.3, + "prob_given_false": 0.15, + }, + { + "platform": "state", + "entity_id": "sensor.colour", + "to_state": "red", + "prob_given_true": 0.4, + "prob_given_false": 0.05, + }, + ], + "prior": 0.1, + } + } + assert len(async_get(hass).issues) == 0 + assert await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + hass.states.async_set("sensor.test_monitored2", "on") + await hass.async_block_till_done() + + assert len(async_get(hass).issues) == 3 + assert ( + async_get(hass).issues[ + ("bayesian", "mirrored_entry/Test_Binary/sensor.test_monitored1") + ] + is not None + ) + + async def test_probability_updates(hass): """Test probability update function.""" prob_given_true = [0.3, 0.6, 0.8] From d756383c5b3a89d4878d8cfd3291041f82c33c1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Sep 2022 10:17:29 -1000 Subject: [PATCH 006/183] Bump yalexs to 1.2.4 (#79222) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index fdd6b52f740..a816ddc06ff 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.3"], + "requirements": ["yalexs==1.2.4"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index aeade9111f8..e02de390571 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2568,7 +2568,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.3 +yalexs==1.2.4 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd4499b9f6f..75ff02697e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.3 +yalexs==1.2.4 # homeassistant.components.yeelight yeelight==0.7.10 From 49114c72e03ba773bc9cf50f36c0f8426bfb437a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 15:43:51 -0400 Subject: [PATCH 007/183] Finish Google brand (#79225) --- homeassistant/brands/google.json | 14 ++- homeassistant/components/nest/manifest.json | 2 +- homeassistant/generated/integrations.json | 94 ++++++++++----------- 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json index c50b0819827..a23c58ed8f1 100644 --- a/homeassistant/brands/google.json +++ b/homeassistant/brands/google.json @@ -1,5 +1,17 @@ { "domain": "google", "name": "Google", - "integrations": ["google", "google_sheets"] + "integrations": [ + "google_assistant", + "google_cloud", + "google_domains", + "google_maps", + "google_pubsub", + "google_sheets", + "google_translate", + "google_travel_time", + "google_wifi", + "google", + "nest" + ] } diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 72e0aed8420..90fad5cf185 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -1,6 +1,6 @@ { "domain": "nest", - "name": "Nest", + "name": "Google Nest", "config_flow": true, "dependencies": ["ffmpeg", "http", "application_credentials"], "after_dependencies": ["media_source"], diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a6d924bbf5b..9cff574ee87 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1581,57 +1581,62 @@ "google": { "name": "Google", "integrations": { - "google": { - "config_flow": true, + "google_assistant": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Google Assistant" + }, + "google_cloud": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Google Cloud Platform" + }, + "google_domains": { + "config_flow": false, "iot_class": "cloud_polling", - "name": "Google Calendar" + "name": "Google Domains" + }, + "google_maps": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Google Maps" + }, + "google_pubsub": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Google Pub/Sub" }, "google_sheets": { "config_flow": true, "iot_class": "cloud_polling", "name": "Google Sheets" + }, + "google_translate": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Google Translate Text-to-Speech" + }, + "google_travel_time": { + "config_flow": true, + "iot_class": "cloud_polling" + }, + "google_wifi": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Google Wifi" + }, + "google": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Google Calendar" + }, + "nest": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Google Nest" } } }, - "google_assistant": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Assistant" - }, - "google_cloud": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Cloud Platform" - }, - "google_domains": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Google Domains" - }, - "google_maps": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Google Maps" - }, - "google_pubsub": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Pub/Sub" - }, - "google_translate": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Translate Text-to-Speech" - }, - "google_travel_time": { - "config_flow": true, - "iot_class": "cloud_polling" - }, - "google_wifi": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Google Wifi" - }, "govee_ble": { "config_flow": true, "iot_class": "local_push", @@ -2784,11 +2789,6 @@ "iot_class": "local_push", "name": "Ness Alarm" }, - "nest": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Nest" - }, "netatmo": { "config_flow": true, "iot_class": "cloud_polling", From 0d462b6a14376844d0b7fe50d3ef83f6d000872d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 16:45:35 -0400 Subject: [PATCH 008/183] Add fritz brand (#79226) --- homeassistant/brands/fritzbox.json | 5 ++++ homeassistant/generated/integrations.json | 31 +++++++++++++---------- 2 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 homeassistant/brands/fritzbox.json diff --git a/homeassistant/brands/fritzbox.json b/homeassistant/brands/fritzbox.json new file mode 100644 index 00000000000..d0c0d1c1584 --- /dev/null +++ b/homeassistant/brands/fritzbox.json @@ -0,0 +1,5 @@ +{ + "domain": "fritzbox", + "name": "FRITZ!Box", + "integrations": ["fritz", "fritzbox", "fritzbox_callmonitor"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9cff574ee87..a54ec563969 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1419,20 +1419,25 @@ "iot_class": "cloud_polling", "name": "Freedompro" }, - "fritz": { - "config_flow": true, - "iot_class": "local_polling", - "name": "AVM FRITZ!Box Tools" - }, "fritzbox": { - "config_flow": true, - "iot_class": "local_polling", - "name": "AVM FRITZ!SmartHome" - }, - "fritzbox_callmonitor": { - "config_flow": true, - "iot_class": "local_polling", - "name": "AVM FRITZ!Box Call Monitor" + "name": "FRITZ!Box", + "integrations": { + "fritz": { + "config_flow": true, + "iot_class": "local_polling", + "name": "AVM FRITZ!Box Tools" + }, + "fritzbox": { + "config_flow": true, + "iot_class": "local_polling", + "name": "AVM FRITZ!SmartHome" + }, + "fritzbox_callmonitor": { + "config_flow": true, + "iot_class": "local_polling", + "name": "AVM FRITZ!Box Call Monitor" + } + } }, "fronius": { "config_flow": true, From 0c05e0e019ba5a9dffe603edc9935f7795780d11 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 16:21:09 -0400 Subject: [PATCH 009/183] Add Apple brand (#79227) --- .pre-commit-config.yaml | 2 +- homeassistant/brands/apple.json | 11 +++++ homeassistant/generated/integrations.json | 51 +++++++++++++---------- script/hassfest/brand.py | 6 +-- 4 files changed, 42 insertions(+), 28 deletions(-) create mode 100644 homeassistant/brands/apple.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8442d7abecc..088099bf4e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -106,7 +106,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/manifest\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ + files: ^(homeassistant/.+/manifest\.json|homeassistant/brands/.+\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ - id: hassfest name: hassfest entry: script/run-in-env.sh python3 -m script.hassfest diff --git a/homeassistant/brands/apple.json b/homeassistant/brands/apple.json new file mode 100644 index 00000000000..1a782b50900 --- /dev/null +++ b/homeassistant/brands/apple.json @@ -0,0 +1,11 @@ +{ + "domain": "apple", + "name": "Apple", + "integrations": [ + "icloud", + "ibeacon", + "apple_tv", + "homekit", + "homekit_controller" + ] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a54ec563969..475b947d073 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -189,10 +189,34 @@ "iot_class": null, "name": "Home Assistant API" }, - "apple_tv": { - "config_flow": true, - "iot_class": "local_push", - "name": "Apple TV" + "apple": { + "name": "Apple", + "integrations": { + "icloud": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Apple iCloud" + }, + "ibeacon": { + "config_flow": true, + "iot_class": "local_push", + "name": "iBeacon Tracker" + }, + "apple_tv": { + "config_flow": true, + "iot_class": "local_push", + "name": "Apple TV" + }, + "homekit": { + "config_flow": true, + "iot_class": "local_push", + "name": "HomeKit" + }, + "homekit_controller": { + "config_flow": true, + "iot_class": "local_push" + } + } }, "application_credentials": { "config_flow": false, @@ -1816,15 +1840,6 @@ "iot_class": null, "name": "Home Assistant Alerts" }, - "homekit": { - "config_flow": true, - "iot_class": "local_push", - "name": "HomeKit" - }, - "homekit_controller": { - "config_flow": true, - "iot_class": "local_push" - }, "homematic": { "config_flow": false, "iot_class": "local_push", @@ -1924,16 +1939,6 @@ "iot_class": "cloud_polling", "name": "Jandy iAqualink" }, - "ibeacon": { - "config_flow": true, - "iot_class": "local_push", - "name": "iBeacon Tracker" - }, - "icloud": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Apple iCloud" - }, "idteck_prox": { "config_flow": false, "iot_class": "local_push", diff --git a/script/hassfest/brand.py b/script/hassfest/brand.py index 64217da1592..80e2495573e 100644 --- a/script/hassfest/brand.py +++ b/script/hassfest/brand.py @@ -51,10 +51,8 @@ def _validate_brand( f"'{sub_integration}' to 'integrations'", ) - if ( - brand.domain in integrations - and not brand.integrations - or brand.domain not in brand.integrations + if brand.domain in integrations and ( + not brand.integrations or brand.domain not in brand.integrations ): config.add_error( "brand", From 0ef73df451351a6f61a86819cb0a424f870e6e04 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 17:09:53 -0400 Subject: [PATCH 010/183] Add Denon brand (#79230) --- homeassistant/brands/denon.json | 5 ++++ homeassistant/generated/integrations.json | 31 +++++++++++++---------- script/hassfest/brand.py | 7 +++-- 3 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 homeassistant/brands/denon.json diff --git a/homeassistant/brands/denon.json b/homeassistant/brands/denon.json new file mode 100644 index 00000000000..a60750e1a31 --- /dev/null +++ b/homeassistant/brands/denon.json @@ -0,0 +1,5 @@ +{ + "domain": "denon", + "name": "Denon", + "integrations": ["denon", "denonavr", "heos"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 475b947d073..5f7fc5dab7a 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -810,14 +810,24 @@ "iot_class": "calculated" }, "denon": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Denon Network Receivers" - }, - "denonavr": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Denon AVR Network Receivers" + "name": "Denon", + "integrations": { + "denon": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Denon Network Receivers" + }, + "denonavr": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Denon AVR Network Receivers" + }, + "heos": { + "config_flow": true, + "iot_class": "local_push", + "name": "Denon HEOS" + } + } }, "deutsche_bahn": { "config_flow": false, @@ -1770,11 +1780,6 @@ "iot_class": "local_polling", "name": "Heatmiser" }, - "heos": { - "config_flow": true, - "iot_class": "local_push", - "name": "Denon HEOS" - }, "here_travel_time": { "config_flow": true, "iot_class": "cloud_polling", diff --git a/script/hassfest/brand.py b/script/hassfest/brand.py index 80e2495573e..c35f50599ff 100644 --- a/script/hassfest/brand.py +++ b/script/hassfest/brand.py @@ -38,7 +38,7 @@ def _validate_brand( if not brand.integrations and not brand.iot_standards: config.add_error( "brand", - f"Invalid brand file {brand.path.name}: At least one of integrations or " + f"{brand.path.name}: At least one of integrations or " "iot_standards must be non-empty", ) @@ -47,8 +47,7 @@ def _validate_brand( if sub_integration not in integrations: config.add_error( "brand", - f"Invalid brand file {brand.path.name}: Can't add non core domain " - f"'{sub_integration}' to 'integrations'", + f"{brand.path.name}: References unknown integration {sub_integration}", ) if brand.domain in integrations and ( @@ -56,7 +55,7 @@ def _validate_brand( ): config.add_error( "brand", - f"Invalid brand file {brand.path.name}: Brand '{brand.brand['domain']}' " + f"{brand.path.name}: Brand '{brand.brand['domain']}' " f"is an integration but is missing in the brand's 'integrations' list'", ) From 9aa6323c46fcee112a40ce04e5a1da863f9ddf04 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 17:09:42 -0400 Subject: [PATCH 011/183] Add Cast + Chat to Google brand (#79231) --- homeassistant/brands/google.json | 4 +++- homeassistant/generated/integrations.json | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json index a23c58ed8f1..8f3340cef29 100644 --- a/homeassistant/brands/google.json +++ b/homeassistant/brands/google.json @@ -12,6 +12,8 @@ "google_travel_time", "google_wifi", "google", - "nest" + "nest", + "cast", + "hangouts" ] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5f7fc5dab7a..c0891bea0a2 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -571,11 +571,6 @@ "iot_class": "cloud_polling", "name": "Canary" }, - "cast": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Google Cast" - }, "cert_expiry": { "config_flow": true, "iot_class": "cloud_polling" @@ -1673,6 +1668,16 @@ "config_flow": true, "iot_class": "cloud_push", "name": "Google Nest" + }, + "cast": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Google Cast" + }, + "hangouts": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Google Chat" } } }, @@ -1735,11 +1740,6 @@ "iot_class": "cloud_polling", "name": "Habitica" }, - "hangouts": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Google Chat" - }, "hardware": { "config_flow": false, "iot_class": null, From 45a217238e8ed1a95859b136a7f90757d5995f13 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 20:30:50 -0400 Subject: [PATCH 012/183] Add ubiquiti brand (#79232) --- homeassistant/brands/ubiquiti.json | 5 +++ .../components/unifi_direct/manifest.json | 2 +- .../components/unifiled/manifest.json | 2 +- homeassistant/generated/integrations.json | 45 ++++++++++--------- 4 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 homeassistant/brands/ubiquiti.json diff --git a/homeassistant/brands/ubiquiti.json b/homeassistant/brands/ubiquiti.json new file mode 100644 index 00000000000..8b64cffaa7e --- /dev/null +++ b/homeassistant/brands/ubiquiti.json @@ -0,0 +1,5 @@ +{ + "domain": "ubiquiti", + "name": "Ubiquiti", + "integrations": ["unifi", "unifi_direct", "unifiled", "unifiprotect"] +} diff --git a/homeassistant/components/unifi_direct/manifest.json b/homeassistant/components/unifi_direct/manifest.json index b3ed7d2ef2f..9bfc2c8ff49 100644 --- a/homeassistant/components/unifi_direct/manifest.json +++ b/homeassistant/components/unifi_direct/manifest.json @@ -1,6 +1,6 @@ { "domain": "unifi_direct", - "name": "Ubiquiti UniFi AP", + "name": "UniFi AP", "documentation": "https://www.home-assistant.io/integrations/unifi_direct", "requirements": ["pexpect==4.6.0"], "codeowners": [], diff --git a/homeassistant/components/unifiled/manifest.json b/homeassistant/components/unifiled/manifest.json index d0716dcec3a..7f3c2b4701b 100644 --- a/homeassistant/components/unifiled/manifest.json +++ b/homeassistant/components/unifiled/manifest.json @@ -1,6 +1,6 @@ { "domain": "unifiled", - "name": "Ubiquiti UniFi LED", + "name": "UniFi LED", "documentation": "https://www.home-assistant.io/integrations/unifiled", "codeowners": ["@florisvdk"], "requirements": ["unifiled==0.11"], diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c0891bea0a2..3e056276c16 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4591,6 +4591,31 @@ "iot_class": "cloud_push", "name": "Twitter" }, + "ubiquiti": { + "name": "Ubiquiti", + "integrations": { + "unifi": { + "config_flow": true, + "iot_class": "local_push", + "name": "UniFi Network" + }, + "unifi_direct": { + "config_flow": false, + "iot_class": "local_polling", + "name": "UniFi AP" + }, + "unifiled": { + "config_flow": false, + "iot_class": "local_polling", + "name": "UniFi LED" + }, + "unifiprotect": { + "config_flow": true, + "iot_class": "local_push", + "name": "UniFi Protect" + } + } + }, "ubus": { "config_flow": false, "iot_class": "local_polling", @@ -4611,26 +4636,6 @@ "iot_class": "cloud_polling", "name": "Ukraine Alarm" }, - "unifi": { - "config_flow": true, - "iot_class": "local_push", - "name": "UniFi Network" - }, - "unifi_direct": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Ubiquiti UniFi AP" - }, - "unifiled": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Ubiquiti UniFi LED" - }, - "unifiprotect": { - "config_flow": true, - "iot_class": "local_push", - "name": "UniFi Protect" - }, "universal": { "config_flow": false, "iot_class": "calculated", From ef8c1130d67aa7d5e17cd3314b6a46bb968c46d1 Mon Sep 17 00:00:00 2001 From: Dennis Schroer Date: Thu, 29 Sep 2022 19:25:23 +0200 Subject: [PATCH 013/183] Update huisbaasje-client 0.1.0 to energyflip-client 0.2.0 (#79233) --- .../components/huisbaasje/__init__.py | 20 ++-- .../components/huisbaasje/config_flow.py | 17 +-- homeassistant/components/huisbaasje/const.py | 2 +- .../components/huisbaasje/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- .../components/huisbaasje/test_config_flow.py | 108 +++++++++++++++--- tests/components/huisbaasje/test_init.py | 16 +-- tests/components/huisbaasje/test_sensor.py | 12 +- 9 files changed, 131 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index fa810d823ca..a3d8863f566 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging import async_timeout -from huisbaasje import Huisbaasje, HuisbaasjeException +from energyflip import EnergyFlip, EnergyFlipException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huisbaasje from a config entry.""" # Create the Huisbaasje client - huisbaasje = Huisbaasje( + energyflip = EnergyFlip( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], source_types=SOURCE_TYPES, @@ -40,13 +40,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Attempt authentication. If this fails, an exception is thrown try: - await huisbaasje.authenticate() - except HuisbaasjeException as exception: + await energyflip.authenticate() + except EnergyFlipException as exception: _LOGGER.error("Authentication failed: %s", str(exception)) return False async def async_update_data(): - return await async_update_huisbaasje(huisbaasje) + return await async_update_huisbaasje(energyflip) # Create a coordinator for polling updates coordinator = DataUpdateCoordinator( @@ -80,17 +80,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_update_huisbaasje(huisbaasje): +async def async_update_huisbaasje(energyflip): """Update the data by performing a request to Huisbaasje.""" try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(FETCH_TIMEOUT): - if not huisbaasje.is_authenticated(): + if not energyflip.is_authenticated(): _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating") - await huisbaasje.authenticate() + await energyflip.authenticate() - current_measurements = await huisbaasje.current_measurements() + current_measurements = await energyflip.current_measurements() return { source_type: { @@ -112,7 +112,7 @@ async def async_update_huisbaasje(huisbaasje): } for source_type in SOURCE_TYPES } - except HuisbaasjeException as exception: + except EnergyFlipException as exception: raise UpdateFailed(f"Error communicating with API: {exception}") from exception diff --git a/homeassistant/components/huisbaasje/config_flow.py b/homeassistant/components/huisbaasje/config_flow.py index 4139b0d75c5..fc3a1c06a15 100644 --- a/homeassistant/components/huisbaasje/config_flow.py +++ b/homeassistant/components/huisbaasje/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Huisbaasje integration.""" import logging -from huisbaasje import Huisbaasje, HuisbaasjeConnectionException, HuisbaasjeException +from energyflip import EnergyFlip, EnergyFlipConnectionException, EnergyFlipException import voluptuous as vol from homeassistant import config_entries @@ -31,10 +31,10 @@ class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: user_id = await self._validate_input(user_input) - except HuisbaasjeConnectionException as exception: + except EnergyFlipConnectionException as exception: _LOGGER.warning(exception) errors["base"] = "cannot_connect" - except HuisbaasjeException as exception: + except EnergyFlipException as exception: _LOGGER.warning(exception) errors["base"] = "invalid_auth" except AbortFlow: @@ -72,9 +72,12 @@ class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] - huisbaasje = Huisbaasje(username, password) + energyflip = EnergyFlip(username, password) - # Attempt authentication. If this fails, an HuisbaasjeException will be thrown - await huisbaasje.authenticate() + # Attempt authentication. If this fails, an EnergyFlipException will be thrown + await energyflip.authenticate() - return huisbaasje.get_user_id() + # Request customer overview. This also sets the user id on the client + await energyflip.customer_overview() + + return energyflip.get_user_id() diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index 637ebd03a17..481f11b2a36 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -1,5 +1,5 @@ """Constants for the Huisbaasje integration.""" -from huisbaasje.const import ( +from energyflip.const import ( SOURCE_TYPE_ELECTRICITY, SOURCE_TYPE_ELECTRICITY_IN, SOURCE_TYPE_ELECTRICITY_IN_LOW, diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index bf3155ed9b8..2963a82512b 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -3,7 +3,7 @@ "name": "Huisbaasje", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", - "requirements": ["huisbaasje-client==0.1.0"], + "requirements": ["energyflip-client==0.2.1"], "codeowners": ["@dennisschroer"], "iot_class": "cloud_polling", "loggers": ["huisbaasje"] diff --git a/requirements_all.txt b/requirements_all.txt index e02de390571..bf4dca981bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,6 +622,9 @@ elmax_api==0.0.2 # homeassistant.components.emulated_roku emulated_roku==0.2.1 +# homeassistant.components.huisbaasje +energyflip-client==0.2.1 + # homeassistant.components.enocean enocean==0.50 @@ -882,9 +885,6 @@ httplib2==0.20.4 # homeassistant.components.huawei_lte huawei-lte-api==1.6.1 -# homeassistant.components.huisbaasje -huisbaasje-client==0.1.0 - # homeassistant.components.hydrawise hydrawiser==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75ff02697e9..bbfb8402eff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -475,6 +475,9 @@ elmax_api==0.0.2 # homeassistant.components.emulated_roku emulated_roku==0.2.1 +# homeassistant.components.huisbaasje +energyflip-client==0.2.1 + # homeassistant.components.enocean enocean==0.50 @@ -659,9 +662,6 @@ httplib2==0.20.4 # homeassistant.components.huawei_lte huawei-lte-api==1.6.1 -# homeassistant.components.huisbaasje -huisbaasje-client==0.1.0 - # homeassistant.components.hyperion hyperion-py==0.7.5 diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 8aac11baf6d..e270079de8c 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -1,11 +1,13 @@ """Test the Huisbaasje config flow.""" from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.huisbaasje.config_flow import ( - HuisbaasjeConnectionException, - HuisbaasjeException, +from energyflip import ( + EnergyFlipConnectionException, + EnergyFlipException, + EnergyFlipUnauthenticatedException, ) + +from homeassistant import config_entries, data_entry_flow from homeassistant.components.huisbaasje.const import DOMAIN from tests.common import MockConfigEntry @@ -21,9 +23,11 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.get_user_id", + "energyflip.EnergyFlip.customer_overview", return_value=None + ) as mock_customer_overview, patch( + "energyflip.EnergyFlip.get_user_id", return_value="test-id", ) as mock_get_user_id, patch( "homeassistant.components.huisbaasje.async_setup_entry", @@ -46,6 +50,7 @@ async def test_form(hass): "password": "test-password", } assert len(mock_authenticate.mock_calls) == 1 + assert len(mock_customer_overview.mock_calls) == 1 assert len(mock_get_user_id.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -57,8 +62,8 @@ async def test_form_invalid_auth(hass): ) with patch( - "huisbaasje.Huisbaasje.authenticate", - side_effect=HuisbaasjeException, + "energyflip.EnergyFlip.authenticate", + side_effect=EnergyFlipException, ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -72,15 +77,15 @@ async def test_form_invalid_auth(hass): assert form_result["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" +async def test_form_authenticate_cannot_connect(hass): + """Test we handle cannot connect error in authenticate.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "huisbaasje.Huisbaasje.authenticate", - side_effect=HuisbaasjeConnectionException, + "energyflip.EnergyFlip.authenticate", + side_effect=EnergyFlipConnectionException, ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -94,14 +99,80 @@ async def test_form_cannot_connect(hass): assert form_result["errors"] == {"base": "cannot_connect"} -async def test_form_unknown_error(hass): - """Test we handle an unknown error.""" +async def test_form_authenticate_unknown_error(hass): + """Test we handle an unknown error in authenticate.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "huisbaasje.Huisbaasje.authenticate", + "energyflip.EnergyFlip.authenticate", + side_effect=Exception, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["errors"] == {"base": "unknown"} + + +async def test_form_customer_overview_cannot_connect(hass): + """Test we handle cannot connect error in customer_overview.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( + "energyflip.EnergyFlip.customer_overview", + side_effect=EnergyFlipConnectionException, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["errors"] == {"base": "cannot_connect"} + + +async def test_form_customer_overview_authentication_error(hass): + """Test we handle an unknown error in customer_overview.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( + "energyflip.EnergyFlip.customer_overview", + side_effect=EnergyFlipUnauthenticatedException, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["errors"] == {"base": "invalid_auth"} + + +async def test_form_customer_overview_unknown_error(hass): + """Test we handle an unknown error in customer_overview.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( + "energyflip.EnergyFlip.customer_overview", side_effect=Exception, ): form_result = await hass.config_entries.flow.async_configure( @@ -133,10 +204,9 @@ async def test_form_entry_exists(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("huisbaasje.Huisbaasje.authenticate", return_value=None), patch( - "huisbaasje.Huisbaasje.get_user_id", - return_value="test-id", - ), patch( + with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( + "energyflip.EnergyFlip.customer_overview", return_value=None + ), patch("energyflip.EnergyFlip.get_user_id", return_value="test-id",), patch( "homeassistant.components.huisbaasje.async_setup_entry", return_value=True, ): diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 859cfc4df83..30de00fd64f 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -1,7 +1,7 @@ """Test cases for the initialisation of the Huisbaasje integration.""" from unittest.mock import patch -from huisbaasje import HuisbaasjeException +from energyflip import EnergyFlipException from homeassistant.components import huisbaasje from homeassistant.config_entries import ConfigEntryState @@ -24,11 +24,11 @@ async def test_setup(hass: HomeAssistant): async def test_setup_entry(hass: HomeAssistant): """Test for successfully setting a config entry.""" with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.is_authenticated", return_value=True + "energyflip.EnergyFlip.is_authenticated", return_value=True ) as mock_is_authenticated, patch( - "huisbaasje.Huisbaasje.current_measurements", + "energyflip.EnergyFlip.current_measurements", return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements: hass.config.components.add(huisbaasje.DOMAIN) @@ -68,7 +68,7 @@ async def test_setup_entry(hass: HomeAssistant): async def test_setup_entry_error(hass: HomeAssistant): """Test for successfully setting a config entry.""" with patch( - "huisbaasje.Huisbaasje.authenticate", side_effect=HuisbaasjeException + "energyflip.EnergyFlip.authenticate", side_effect=EnergyFlipException ) as mock_authenticate: hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( @@ -103,11 +103,11 @@ async def test_setup_entry_error(hass: HomeAssistant): async def test_unload_entry(hass: HomeAssistant): """Test for successfully unloading the config entry.""" with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.is_authenticated", return_value=True + "energyflip.EnergyFlip.is_authenticated", return_value=True ) as mock_is_authenticated, patch( - "huisbaasje.Huisbaasje.current_measurements", + "energyflip.EnergyFlip.current_measurements", return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements: hass.config.components.add(huisbaasje.DOMAIN) diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 84e1f71071c..43789988003 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -29,11 +29,11 @@ from tests.common import MockConfigEntry async def test_setup_entry(hass: HomeAssistant): """Test for successfully loading sensor states.""" with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.is_authenticated", return_value=True + "energyflip.EnergyFlip.is_authenticated", return_value=True ) as mock_is_authenticated, patch( - "huisbaasje.Huisbaasje.current_measurements", + "energyflip.EnergyFlip.current_measurements", return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements: @@ -344,11 +344,11 @@ async def test_setup_entry(hass: HomeAssistant): async def test_setup_entry_absent_measurement(hass: HomeAssistant): """Test for successfully loading sensor states when response does not contain all measurements.""" with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.is_authenticated", return_value=True + "energyflip.EnergyFlip.is_authenticated", return_value=True ) as mock_is_authenticated, patch( - "huisbaasje.Huisbaasje.current_measurements", + "energyflip.EnergyFlip.current_measurements", return_value=MOCK_LIMITED_CURRENT_MEASUREMENTS, ) as mock_current_measurements: From ef0de81c6895051956f4716b3fd57270eadadb70 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Sep 2022 06:28:51 -0400 Subject: [PATCH 014/183] Add Leviton brand (#79244) --- .prettierignore | 1 + homeassistant/brands/leviton.json | 5 +++++ homeassistant/components/zwave_js/manifest.json | 5 +---- homeassistant/generated/integrations.json | 6 ++++++ homeassistant/generated/supported_brands.py | 1 - 5 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 homeassistant/brands/leviton.json diff --git a/.prettierignore b/.prettierignore index 950741ec8b2..a4d1d99079d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ azure-*.yml docs/source/_templates/* homeassistant/components/*/translations/*.json +homeassistant/generated/* diff --git a/homeassistant/brands/leviton.json b/homeassistant/brands/leviton.json new file mode 100644 index 00000000000..b6d78586c1b --- /dev/null +++ b/homeassistant/brands/leviton.json @@ -0,0 +1,5 @@ +{ + "domain": "leviton", + "name": "Leviton", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 9880d5bb5d1..8f0c93f6c3e 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -21,8 +21,5 @@ } ], "zeroconf": ["_zwave-js-server._tcp.local."], - "loggers": ["zwave_js_server"], - "supported_brands": { - "leviton_z_wave": "Leviton Z-Wave" - } + "loggers": ["zwave_js_server"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3e056276c16..b9561e6c24a 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2267,6 +2267,12 @@ "iot_class": "local_polling", "name": "LED BLE" }, + "leviton": { + "name": "Leviton", + "iot_standards": [ + "zwave" + ] + }, "lg_netcast": { "config_flow": false, "iot_class": "local_polling", diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 39f7c6bea05..50490d2c847 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -13,5 +13,4 @@ HAS_SUPPORTED_BRANDS = [ "thermobeacon", "wemo", "yalexs_ble", - "zwave_js", ] From 92d2210a81e545e85c77296a8586b8d90eb4baf0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Sep 2022 08:34:51 -0400 Subject: [PATCH 015/183] Add DialogFlow to Google brand (#79245) --- homeassistant/brands/google.json | 3 ++- homeassistant/generated/integrations.json | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json index 8f3340cef29..5f37de46180 100644 --- a/homeassistant/brands/google.json +++ b/homeassistant/brands/google.json @@ -14,6 +14,7 @@ "google", "nest", "cast", - "hangouts" + "hangouts", + "dialogflow" ] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b9561e6c24a..dd40bb050fd 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -867,11 +867,6 @@ "config_flow": false, "iot_class": null }, - "dialogflow": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Dialogflow" - }, "digital_ocean": { "config_flow": false, "iot_class": "local_polling", @@ -1678,6 +1673,11 @@ "config_flow": true, "iot_class": "cloud_push", "name": "Google Chat" + }, + "dialogflow": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Dialogflow" } } }, From 0e02d378159686ecaebb4d5830169083574b988c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Sep 2022 02:42:55 -1000 Subject: [PATCH 016/183] Wait for disconnect when we are out of connection ble slots in esphome (#79246) --- .../components/esphome/bluetooth/client.py | 42 ++++++++++++++++++- .../components/esphome/entry_data.py | 15 +++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 2eb722bdddf..8e8d7cf6427 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -8,6 +8,7 @@ from typing import Any, TypeVar, cast import uuid from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError +import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback from bleak.backends.device import BLEDevice @@ -24,6 +25,10 @@ from .service import BleakGATTServiceESPHome DEFAULT_MTU = 23 GATT_HEADER_SIZE = 3 +DISCONNECT_TIMEOUT = 5.0 +CONNECT_FREE_SLOT_TIMEOUT = 2.0 +GATT_READ_TIMEOUT = 30.0 + DEFAULT_MAX_WRITE_WITHOUT_RESPONSE = DEFAULT_MTU - GATT_HEADER_SIZE _LOGGER = logging.getLogger(__name__) @@ -37,6 +42,19 @@ def mac_to_int(address: str) -> int: return int(address.replace(":", ""), 16) +def verify_connected(func: _WrapFuncType) -> _WrapFuncType: + """Define a wrapper throw BleakError if not connected.""" + + async def _async_wrap_bluetooth_connected_operation( + self: "ESPHomeClient", *args: Any, **kwargs: Any + ) -> Any: + if not self._is_connected: # pylint: disable=protected-access + raise BleakError("Not connected") + return await func(self, *args, **kwargs) + + return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation) + + def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: """Define a wrapper throw esphome api errors as BleakErrors.""" @@ -128,6 +146,7 @@ class ESPHomeClient(BaseBleakClient): Returns: Boolean representing connection status. """ + await self._wait_for_free_connection_slot(CONNECT_FREE_SLOT_TIMEOUT) connected_future: asyncio.Future[bool] = asyncio.Future() @@ -179,8 +198,20 @@ class ESPHomeClient(BaseBleakClient): """Disconnect from the peripheral device.""" self._unsubscribe_connection_state() await self._client.bluetooth_device_disconnect(self._address_as_int) + await self._wait_for_free_connection_slot(DISCONNECT_TIMEOUT) return True + async def _wait_for_free_connection_slot(self, timeout: float) -> None: + """Wait for a free connection slot.""" + entry_data = self._async_get_entry_data() + if entry_data.ble_connections_free: + return + _LOGGER.debug( + "%s: Out of connection slots, waiting for a free one", self._source + ) + async with async_timeout.timeout(timeout): + await entry_data.wait_for_ble_connections_free() + @property def is_connected(self) -> bool: """Is Connected.""" @@ -191,11 +222,13 @@ class ESPHomeClient(BaseBleakClient): """Get ATT MTU size for active connection.""" return self._mtu or DEFAULT_MTU + @verify_connected @api_error_as_bleak_error async def pair(self, *args: Any, **kwargs: Any) -> bool: """Attempt to pair.""" raise NotImplementedError("Pairing is not available in ESPHome.") + @verify_connected @api_error_as_bleak_error async def unpair(self) -> bool: """Attempt to unpair.""" @@ -272,6 +305,7 @@ class ESPHomeClient(BaseBleakClient): raise BleakError(f"Characteristic {char_specifier} was not found!") return characteristic + @verify_connected @api_error_as_bleak_error async def read_gatt_char( self, @@ -289,9 +323,10 @@ class ESPHomeClient(BaseBleakClient): """ characteristic = self._resolve_characteristic(char_specifier) return await self._client.bluetooth_gatt_read( - self._address_as_int, characteristic.handle + self._address_as_int, characteristic.handle, GATT_READ_TIMEOUT ) + @verify_connected @api_error_as_bleak_error async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: """Perform read operation on the specified GATT descriptor. @@ -302,9 +337,10 @@ class ESPHomeClient(BaseBleakClient): (bytearray) The read data. """ return await self._client.bluetooth_gatt_read_descriptor( - self._address_as_int, handle + self._address_as_int, handle, GATT_READ_TIMEOUT ) + @verify_connected @api_error_as_bleak_error async def write_gatt_char( self, @@ -326,6 +362,7 @@ class ESPHomeClient(BaseBleakClient): self._address_as_int, characteristic.handle, bytes(data), response ) + @verify_connected @api_error_as_bleak_error async def write_gatt_descriptor( self, handle: int, data: bytes | bytearray | memoryview @@ -340,6 +377,7 @@ class ESPHomeClient(BaseBleakClient): self._address_as_int, handle, bytes(data) ) + @verify_connected @api_error_as_bleak_error async def start_notify( self, diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d85e12845da..ac2a148d899 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -89,6 +89,9 @@ class RuntimeEntryData: _storage_contents: dict[str, Any] | None = None ble_connections_free: int = 0 ble_connections_limit: int = 0 + _ble_connection_free_futures: list[asyncio.Future[int]] = field( + default_factory=list + ) @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: @@ -97,6 +100,18 @@ class RuntimeEntryData: _LOGGER.debug("%s: BLE connection limits: %s/%s", name, free, limit) self.ble_connections_free = free self.ble_connections_limit = limit + if free: + for fut in self._ble_connection_free_futures: + fut.set_result(free) + self._ble_connection_free_futures.clear() + + async def wait_for_ble_connections_free(self) -> int: + """Wait until there are free BLE connections.""" + if self.ble_connections_free > 0: + return self.ble_connections_free + fut: asyncio.Future[int] = asyncio.Future() + self._ble_connection_free_futures.append(fut) + return await fut @callback def async_remove_entity( From ddd981d4feb7c5890e3ab2f38ee31f9e3c1f477f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:38:06 +0200 Subject: [PATCH 017/183] Use SensorDeviceClass.VOLUME in components (#79253) --- homeassistant/components/flo/sensor.py | 1 + homeassistant/components/justnimbus/sensor.py | 5 +++++ homeassistant/components/kegtron/sensor.py | 3 +++ homeassistant/components/overkiz/sensor.py | 3 +++ homeassistant/components/p1_monitor/sensor.py | 1 + homeassistant/components/streamlabswater/sensor.py | 4 +++- homeassistant/components/suez_water/sensor.py | 7 ++++++- homeassistant/components/surepetcare/sensor.py | 1 + 8 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index e7fbd293bd1..9f793b749e4 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -67,6 +67,7 @@ async def async_setup_entry( class FloDailyUsageSensor(FloEntity, SensorEntity): """Monitors the daily water usage.""" + _attr_device_class = SensorDeviceClass.VOLUME _attr_icon = WATER_ICON _attr_native_unit_of_measurement = VOLUME_GALLONS _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py index 73a68ac9139..41f1e81d5b3 100644 --- a/homeassistant/components/justnimbus/sensor.py +++ b/homeassistant/components/justnimbus/sensor.py @@ -103,6 +103,7 @@ SENSOR_TYPES = ( name="Reservoir content", icon="mdi:car-coolant-level", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.reservoir_content, @@ -112,6 +113,7 @@ SENSOR_TYPES = ( name="Total saved", icon="mdi:water-opacity", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.total_saved, @@ -121,6 +123,7 @@ SENSOR_TYPES = ( name="Total replenished", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.total_replenished, @@ -138,6 +141,7 @@ SENSOR_TYPES = ( name="Total use", icon="mdi:chart-donut", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.totver, @@ -147,6 +151,7 @@ SENSOR_TYPES = ( name="Max reservoir content", icon="mdi:waves", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.reservoir_content_max, diff --git a/homeassistant/components/kegtron/sensor.py b/homeassistant/components/kegtron/sensor.py index f52a7c79634..892d8651185 100644 --- a/homeassistant/components/kegtron/sensor.py +++ b/homeassistant/components/kegtron/sensor.py @@ -39,6 +39,7 @@ SENSOR_DESCRIPTIONS = { key=KegtronSensorDeviceClass.KEG_SIZE, icon="mdi:keg", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), KegtronSensorDeviceClass.KEG_TYPE: SensorEntityDescription( @@ -49,12 +50,14 @@ SENSOR_DESCRIPTIONS = { key=KegtronSensorDeviceClass.VOLUME_START, icon="mdi:keg", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), KegtronSensorDeviceClass.VOLUME_DISPENSED: SensorEntityDescription( key=KegtronSensorDeviceClass.VOLUME_DISPENSED, icon="mdi:keg", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL, ), KegtronSensorDeviceClass.PORT_STATE: SensorEntityDescription( diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 83f123eaad7..e80e08e263a 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -90,6 +90,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ name="Water volume estimation at 40 °C", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), @@ -98,6 +99,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ name="Water consumption", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( @@ -105,6 +107,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ name="Outlet engine", icon="mdi:fan-chevron-down", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 757bf249ca1..e55c8dacea5 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -222,6 +222,7 @@ SENSORS_WATERMETER: tuple[SensorEntityDescription, ...] = ( name="Consumption Day", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, ), SensorEntityDescription( key="consumption_total", diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index afef8070fcb..1a1070d1ea8 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import VOLUME_GALLONS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -80,6 +80,8 @@ class StreamlabsUsageData: class StreamLabsDailyUsage(SensorEntity): """Monitors the daily water usage.""" + _attr_device_class = SensorDeviceClass.VOLUME + def __init__(self, location_name, streamlabs_usage_data): """Initialize the daily water usage device.""" self._location_name = location_name diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index ac691829236..77b1de6555e 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -8,7 +8,11 @@ from pysuez import SuezClient from pysuez.client import PySuezError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, VOLUME_LITERS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -63,6 +67,7 @@ class SuezSensor(SensorEntity): _attr_name = NAME _attr_icon = ICON _attr_native_unit_of_measurement = VOLUME_LITERS + _attr_device_class = SensorDeviceClass.VOLUME def __init__(self, client): """Initialize the data object.""" diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index e9967054900..1ae22710060 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -87,6 +87,7 @@ class SureBattery(SurePetcareEntity, SensorEntity): class Felaqua(SurePetcareEntity, SensorEntity): """Sure Petcare Felaqua.""" + _attr_device_class = SensorDeviceClass.VOLUME _attr_native_unit_of_measurement = VOLUME_MILLILITERS def __init__( From d552056c17882f322b39dbf90af2f7cf36e44c61 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:20:13 +0200 Subject: [PATCH 018/183] Use SensorDeviceClass.SPEED in rfxtrx (#79261) Use SensorDeviceClass.VOLUME in rfxtrx --- homeassistant/components/rfxtrx/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index b4d4d65295c..563b166e0aa 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -206,12 +206,14 @@ SENSOR_TYPES = ( name="Wind average speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, ), RfxtrxSensorEntityDescription( key="Wind gust", name="Wind gust", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, ), RfxtrxSensorEntityDescription( key="Rain total", From b17413f152c09b83c69d2a323bc7d92d974db463 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:37:21 +0200 Subject: [PATCH 019/183] Use SensorDeviceClass.SPEED in components (#79262) --- homeassistant/components/ambient_station/sensor.py | 5 +++++ homeassistant/components/buienradar/sensor.py | 7 +++++++ homeassistant/components/environment_canada/sensor.py | 2 ++ homeassistant/components/homematic/sensor.py | 1 + homeassistant/components/homematicip_cloud/sensor.py | 2 ++ homeassistant/components/lacrosse_view/sensor.py | 1 + homeassistant/components/netatmo/sensor.py | 2 ++ homeassistant/components/tellduslive/sensor.py | 2 ++ .../components/trafikverket_weatherstation/sensor.py | 2 ++ 9 files changed, 24 insertions(+) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index a04c279915f..65c726bfff3 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -308,6 +308,7 @@ SENSOR_DESCRIPTIONS = ( name="Max gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -631,6 +632,7 @@ SENSOR_DESCRIPTIONS = ( name="Wind gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -638,18 +640,21 @@ SENSOR_DESCRIPTIONS = ( name="Wind avg 10m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG2M, name="Wind avg 2m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, ), SensorEntityDescription( key=TYPE_WINDSPEEDMPH, name="Wind speed", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 2f3e60b0646..08303120a92 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -138,6 +138,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="windspeed", name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), @@ -175,6 +176,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="windgust", name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( @@ -463,30 +465,35 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="windspeed_1d", name="Wind speed 1d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_2d", name="Wind speed 2d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_3d", name="Wind speed 3d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_4d", name="Wind speed 4d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_5d", name="Wind speed 5d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 08da60fe01f..5f22b251493 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -198,6 +198,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="wind_gust", name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_gust", {}).get("value"), ), @@ -205,6 +206,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="wind_speed", name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_speed", {}).get("value"), ), diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 456a10b7630..c7a78c7bbcf 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -170,6 +170,7 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { "WIND_SPEED": SensorEntityDescription( key="WIND_SPEED", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), "WIND_DIRECTION": SensorEntityDescription( diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 57a8b7bd714..bb9dd8021ed 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -344,6 +344,8 @@ class HomematicipEnergySensor(HomematicipGenericEntity, SensorEntity): class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP wind speed sensor.""" + _attr_device_class = SensorDeviceClass.SPEED + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the windspeed sensor.""" super().__init__(hap, device, post="Windspeed") diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 0f60ef4ca10..1ff3e78812f 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -81,6 +81,7 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, ), "Rain": LaCrosseSensorEntityDescription( key="Rain", diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index ff555ecd472..65ac610ef5d 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -200,6 +200,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="wind_strength", entity_registry_enabled_default=True, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), @@ -225,6 +226,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="gust_strength", entity_registry_enabled_default=False, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 8d02763d428..e2995620fb1 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -76,12 +76,14 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key=SENSOR_TYPE_WINDAVERAGE, name="Wind average", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_WINDGUST: SensorEntityDescription( key=SENSOR_TYPE_WINDGUST, name="Wind gust", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_UV: SensorEntityDescription( diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 68c47e9320c..4053ce7cbc4 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -89,6 +89,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( api_key="windforce", name="Wind speed", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), @@ -97,6 +98,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( api_key="windforcemax", name="Wind speed max", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy-variant", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, From 7b026a8eaf078359fe844650ee2c76ce8ca2c930 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:19:20 +0200 Subject: [PATCH 020/183] Use SensorDeviceClass.SPEED in metoffice (#79263) --- homeassistant/components/metoffice/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index dd8ceefad23..c553904a895 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -83,15 +83,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wind_speed", name="Wind speed", - device_class=None, native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", entity_registry_enabled_default=True, ), SensorEntityDescription( key="wind_direction", name="Wind direction", - device_class=None, native_unit_of_measurement=None, icon="mdi:compass-outline", entity_registry_enabled_default=False, @@ -99,8 +98,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wind_gust", name="Wind gust", - device_class=None, native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", entity_registry_enabled_default=False, ), From 8342964345b29a6c891d2e71ddb70ffc73f70fd0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:28:59 +0200 Subject: [PATCH 021/183] Use SensorDeviceClass.WEIGHT in components (#79277) --- homeassistant/components/bthome/sensor.py | 4 ++-- homeassistant/components/litterrobot/sensor.py | 1 + homeassistant/components/mysensors/sensor.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index d80763d4600..9d68ce2d3b4 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -145,14 +145,14 @@ SENSOR_DESCRIPTIONS = { # Used for mass sensor with kg unit (BTHomeSensorDeviceClass.MASS, Units.MASS_KILOGRAMS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_KILOGRAMS}", - device_class=None, + device_class=SensorDeviceClass.WEIGHT, native_unit_of_measurement=MASS_KILOGRAMS, state_class=SensorStateClass.MEASUREMENT, ), # Used for mass sensor with lb unit (BTHomeSensorDeviceClass.MASS, Units.MASS_POUNDS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_POUNDS}", - device_class=None, + device_class=SensorDeviceClass.WEIGHT, native_unit_of_measurement=MASS_POUNDS, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 1a8f066f54b..b9d70528cab 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -111,6 +111,7 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { name="Pet weight", icon="mdi:scale", native_unit_of_measurement=MASS_POUNDS, + device_class=SensorDeviceClass.WEIGHT, ), ], FeederRobot: [ diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 6f940c5d625..dd10c203228 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -94,6 +94,7 @@ SENSORS: dict[str, SensorEntityDescription] = { "V_WEIGHT": SensorEntityDescription( key="V_WEIGHT", native_unit_of_measurement=MASS_KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, icon="mdi:weight-kilogram", ), "V_DISTANCE": SensorEntityDescription( From 8c2cfe0281b1951c2cdaa01d48082e9af4a6b4d9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 12:19:34 +0200 Subject: [PATCH 022/183] Use SensorDeviceClass.DISTANCE in components (#79285) * Use SensorDeviceClass.DISTANCE in components * Adjust mysensors --- homeassistant/components/buienradar/sensor.py | 1 + homeassistant/components/environment_canada/sensor.py | 1 + homeassistant/components/metoffice/sensor.py | 2 +- homeassistant/components/mysensors/sensor.py | 1 + homeassistant/components/opengarage/sensor.py | 1 + homeassistant/components/starline/sensor.py | 1 + homeassistant/components/wallbox/sensor.py | 1 + 7 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 08303120a92..279fdc145d5 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -170,6 +170,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="visibility", name="Visibility", native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 5f22b251493..88ec055ad03 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -172,6 +172,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="visibility", name="Visibility", native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("visibility", {}).get("value"), ), diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index c553904a895..77532b379b6 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -114,8 +114,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="visibility_distance", name="Visibility distance", - device_class=None, native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, icon="mdi:eye", entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index dd10c203228..59c33a48884 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -100,6 +100,7 @@ SENSORS: dict[str, SensorEntityDescription] = { "V_DISTANCE": SensorEntityDescription( key="V_DISTANCE", native_unit_of_measurement=LENGTH_METERS, + device_class=SensorDeviceClass.DISTANCE, icon="mdi:ruler", ), "V_IMPEDANCE": SensorEntityDescription( diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index d09ed130152..bf75cd34998 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -29,6 +29,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="dist", native_unit_of_measurement=LENGTH_CENTIMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index d4ea2d02555..588b9f93e08 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -81,6 +81,7 @@ SENSOR_TYPES: tuple[StarlineSensorEntityDescription, ...] = ( key="mileage", name_="Mileage", native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, icon="mdi:counter", ), ) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 2c4a8c67bed..1fae68da2e4 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -85,6 +85,7 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { name="Added Range", precision=0, native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.TOTAL_INCREASING, ), CHARGER_ADDED_ENERGY_KEY: WallboxSensorEntityDescription( From bae41d0702ae0bfee86a0d927e649166b93cb985 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 29 Sep 2022 17:22:30 +0000 Subject: [PATCH 023/183] Check if `new_version` is not empty string in Shelly update platform (#79300) --- homeassistant/components/shelly/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 63972e9456d..fa37b394b6c 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -163,7 +163,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): new_version = self.entity_description.latest_version( self.wrapper.device.status, ) - if new_version is not None: + if new_version not in (None, ""): return cast(str, new_version) return self.installed_version From 1e7f79ff99eb887492ee4c6945df46d5afadf326 Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Thu, 29 Sep 2022 17:24:06 +0000 Subject: [PATCH 024/183] Add repair for missing Bayesian `prob_given_false` (#79303) --- .../components/bayesian/binary_sensor.py | 19 ++++++-- homeassistant/components/bayesian/repairs.py | 17 ++++++- .../components/bayesian/strings.json | 12 +++++ .../components/bayesian/translations/en.json | 4 ++ .../components/bayesian/test_binary_sensor.py | 44 +++++++++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/bayesian/strings.json diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 0e943b2d0ad..706c7ecdfd7 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict import logging +from typing import Any import voluptuous as vol @@ -34,7 +35,7 @@ from homeassistant.helpers.template import result_as_boolean from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS -from .repairs import raise_mirrored_entries +from .repairs import raise_mirrored_entries, raise_no_prob_given_false ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" @@ -62,7 +63,7 @@ NUMERIC_STATE_SCHEMA = vol.Schema( vol.Optional(CONF_ABOVE): vol.Coerce(float), vol.Optional(CONF_BELOW): vol.Coerce(float), vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Required(CONF_P_GIVEN_F): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), }, required=True, ) @@ -73,7 +74,7 @@ STATE_SCHEMA = vol.Schema( vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TO_STATE): cv.string, vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Required(CONF_P_GIVEN_F): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), }, required=True, ) @@ -83,7 +84,7 @@ TEMPLATE_SCHEMA = vol.Schema( CONF_PLATFORM: CONF_TEMPLATE, vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Required(CONF_P_GIVEN_F): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), }, required=True, ) @@ -128,6 +129,16 @@ async def async_setup_platform( probability_threshold = config[CONF_PROBABILITY_THRESHOLD] device_class = config.get(CONF_DEVICE_CLASS) + # Should deprecate in some future version (2022.10 at time of writing) & make prob_given_false required in schemas. + broken_observations: list[dict[str, Any]] = [] + for observation in observations: + if CONF_P_GIVEN_F not in observation: + text: str = f"{name}/{observation.get(CONF_ENTITY_ID,'')}{observation.get(CONF_VALUE_TEMPLATE,'')}" + raise_no_prob_given_false(hass, observation, text) + _LOGGER.error("Missing prob_given_false YAML entry for %s", text) + broken_observations.append(observation) + observations = [x for x in observations if x not in broken_observations] + async_add_entities( [ BayesianBinarySensor( diff --git a/homeassistant/components/bayesian/repairs.py b/homeassistant/components/bayesian/repairs.py index a1391f8c550..a1d4f142527 100644 --- a/homeassistant/components/bayesian/repairs.py +++ b/homeassistant/components/bayesian/repairs.py @@ -31,9 +31,24 @@ def raise_mirrored_entries(hass: HomeAssistant, observations, text: str = "") -> "mirrored_entry/" + text, breaks_in_ha_version="2022.10.0", is_fixable=False, - is_persistent=False, severity=issue_registry.IssueSeverity.WARNING, translation_key="manual_migration", translation_placeholders={"entity": text}, learn_more_url="https://github.com/home-assistant/core/pull/67631", ) + + +# Should deprecate in some future version (2022.10 at time of writing) & make prob_given_false required in schemas. +def raise_no_prob_given_false(hass: HomeAssistant, observation, text: str) -> None: + """In previous 2022.9 and earlier, prob_given_false was optional and had a default version.""" + issue_registry.async_create_issue( + hass, + DOMAIN, + f"no_prob_given_false/{text}", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=issue_registry.IssueSeverity.ERROR, + translation_key="no_prob_given_false", + translation_placeholders={"entity": text}, + learn_more_url="https://github.com/home-assistant/core/pull/67631", + ) diff --git a/homeassistant/components/bayesian/strings.json b/homeassistant/components/bayesian/strings.json new file mode 100644 index 00000000000..338795624cd --- /dev/null +++ b/homeassistant/components/bayesian/strings.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", + "title": "Manual YAML fix required for Bayesian" + }, + "no_prob_given_false": { + "description": "In the Bayesian integration `prob_given_false` is now a required configuration variable as there was no mathematical rationale for the previous default value. Please add this to your `configuration.yml` for `bayesian/{entity}`. These observations will be ignored until you do.", + "title": "Manual YAML addition required for Bayesian" + } + } +} diff --git a/homeassistant/components/bayesian/translations/en.json b/homeassistant/components/bayesian/translations/en.json index ae9e5645f73..f95e153d986 100644 --- a/homeassistant/components/bayesian/translations/en.json +++ b/homeassistant/components/bayesian/translations/en.json @@ -3,6 +3,10 @@ "manual_migration": { "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", "title": "Manual YAML fix required for Bayesian" + }, + "no_prob_given_false": { + "description": "In the Bayesian integration `prob_given_false` is now a required configuration variable as there was no mathematical rationale for the previous default value. Please add this to your `configuration.yml` for `bayesian/{entity}`. These observations will be ignored until you do.", + "title": "Manual YAML addition required for Bayesian" } } } diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 0344e2b9445..e16033c66a2 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -587,6 +587,50 @@ async def test_mirrored_observations(hass): ) +async def test_missing_prob_given_false(hass): + """Test whether missing prob_given_false are detected and appropriate issues are created.""" + + config = { + "binary_sensor": { + "platform": "bayesian", + "name": "missingpgf", + "observations": [ + { + "platform": "state", + "entity_id": "binary_sensor.test_monitored", + "to_state": "on", + "prob_given_true": 0.8, + }, + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored2') == 'off'}}", + "prob_given_true": 0.79, + }, + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored1", + "above": 5, + "prob_given_true": 0.7, + }, + ], + "prior": 0.1, + } + } + assert len(async_get(hass).issues) == 0 + assert await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + hass.states.async_set("sensor.test_monitored2", "on") + await hass.async_block_till_done() + + assert len(async_get(hass).issues) == 3 + assert ( + async_get(hass).issues[ + ("bayesian", "no_prob_given_false/missingpgf/sensor.test_monitored1") + ] + is not None + ) + + async def test_probability_updates(hass): """Test probability update function.""" prob_given_true = [0.3, 0.6, 0.8] From 2df05ee35341a0a2a0649a1ab24f1c6646073140 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Sep 2022 08:58:16 -0600 Subject: [PATCH 025/183] Use correct exception type for RainMachine select API error (#79309) --- homeassistant/components/rainmachine/select.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py index 82dddfb8f3a..0f3b8d10be2 100644 --- a/homeassistant/components/rainmachine/select.py +++ b/homeassistant/components/rainmachine/select.py @@ -9,6 +9,7 @@ from homeassistant.components.select import SelectEntity, SelectEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -145,7 +146,7 @@ class FreezeProtectionTemperatureSelect(RainMachineEntity, SelectEntity): {self.entity_description.data_key: self._label_to_api_value_map[option]} ) except RainMachineError as err: - raise ValueError(f"Error while setting {self.name}: {err}") from err + raise HomeAssistantError(f"Error while setting {self.name}: {err}") from err @callback def update_from_latest_data(self) -> None: From 482ce758e4e2eac1299767ddd5ab8307d0780ddb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Sep 2022 11:22:28 -0500 Subject: [PATCH 026/183] Don't create Repairs issue on RainMachine entity replacement (#79310) * Don't create Repairs issue on RainMachine entity replacement * Strings --- .../components/rainmachine/strings.json | 13 ------------ .../rainmachine/translations/en.json | 13 ------------ homeassistant/components/rainmachine/util.py | 20 +------------------ 3 files changed, 1 insertion(+), 45 deletions(-) diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 95b92e99294..7634c0a69c5 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -27,18 +27,5 @@ } } } - }, - "issues": { - "replaced_old_entity": { - "title": "The {old_entity_id} entity will be removed", - "fix_flow": { - "step": { - "confirm": { - "title": "The {old_entity_id} entity will be removed", - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`." - } - } - } - } } } diff --git a/homeassistant/components/rainmachine/translations/en.json b/homeassistant/components/rainmachine/translations/en.json index 3e5d824ee08..9369eeae4c8 100644 --- a/homeassistant/components/rainmachine/translations/en.json +++ b/homeassistant/components/rainmachine/translations/en.json @@ -18,19 +18,6 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", - "title": "The {old_entity_id} entity will be removed" - } - } - }, - "title": "The {old_entity_id} entity will be removed" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index 3c66d530cf4..67ffc83d5bd 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -14,10 +14,9 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, LOGGER +from .const import LOGGER SIGNAL_REBOOT_COMPLETED = "rainmachine_reboot_completed_{0}" SIGNAL_REBOOT_REQUESTED = "rainmachine_reboot_requested_{0}" @@ -70,23 +69,6 @@ def async_finish_entity_domain_replacements( continue old_entity_id = registry_entry.entity_id - translation_key = "replaced_old_entity" - - async_create_issue( - hass, - DOMAIN, - f"{translation_key}_{old_entity_id}", - breaks_in_ha_version=strategy.breaks_in_ha_version, - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders={ - "old_entity_id": old_entity_id, - "replacement_entity_id": strategy.replacement_entity_id, - }, - ) - if strategy.remove_old_entity: LOGGER.info('Removing old entity: "%s"', old_entity_id) ent_reg.async_remove(old_entity_id) From fda831e17b9ae0ada20635956aa1d7dbc7a1afee Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Sep 2022 12:24:52 -0500 Subject: [PATCH 027/183] Don't create Repairs issue on Guardian entity replacement (#79311) --- .../components/guardian/strings.json | 11 ---------- .../components/guardian/translations/en.json | 11 ---------- homeassistant/components/guardian/util.py | 20 +------------------ 3 files changed, 1 insertion(+), 41 deletions(-) diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index 33ddcf637a4..683f13c8d36 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -29,17 +29,6 @@ } } } - }, - "replaced_old_entity": { - "title": "The {old_entity_id} entity will be removed", - "fix_flow": { - "step": { - "confirm": { - "title": "The {old_entity_id} entity will be removed", - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`." - } - } - } } } } diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index ac87ae36506..1aaf8b888c8 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -29,17 +29,6 @@ } }, "title": "The {deprecated_service} service will be removed" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", - "title": "The {old_entity_id} entity will be removed" - } - } - }, - "title": "The {old_entity_id} entity will be removed" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 9966435e7b0..250fee58db5 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -14,10 +14,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, LOGGER +from .const import LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) @@ -56,23 +55,6 @@ def async_finish_entity_domain_replacements( continue old_entity_id = registry_entry.entity_id - translation_key = "replaced_old_entity" - - async_create_issue( - hass, - DOMAIN, - f"{translation_key}_{old_entity_id}", - breaks_in_ha_version=strategy.breaks_in_ha_version, - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders={ - "old_entity_id": old_entity_id, - "replacement_entity_id": strategy.replacement_entity_id, - }, - ) - if strategy.remove_old_entity: LOGGER.info('Removing old entity: "%s"', old_entity_id) ent_reg.async_remove(old_entity_id) From 0c7eb3b0846f4ef38c2486c5054740c427b11ac2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 29 Sep 2022 19:22:41 +0200 Subject: [PATCH 028/183] Update frontend to 20220929.0 (#79317) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f45372ac302..bcc574a4cad 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220928.0"], + "requirements": ["home-assistant-frontend==20220929.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b1ced2c220..38b6ee1e52b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.17.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220928.0 +home-assistant-frontend==20220929.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index bf4dca981bb..74cce746d09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,7 +865,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20220928.0 +home-assistant-frontend==20220929.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bbfb8402eff..d1d5a1f3618 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -645,7 +645,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20220928.0 +home-assistant-frontend==20220929.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 3909fa70d58fd757efc331526d047ed516f1e883 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Sep 2022 13:44:27 -0400 Subject: [PATCH 029/183] Bumped version to 2022.10.0b1 --- 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 22f66ed452b..3d1dce999b0 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 = 10 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __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 0fb94710112..e6037e4734c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0b0" +version = "2022.10.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e3d4a7cf22e6d8994f071147e68d3ca9c47a5802 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Sep 2022 22:21:00 -0400 Subject: [PATCH 030/183] Store alternative domain for Zeroconf homekit discovery (#79240) --- homeassistant/components/zeroconf/__init__.py | 10 +++++++++- tests/components/zeroconf/test_init.py | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index e6c635dc308..5a2fc61f897 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -404,6 +404,7 @@ class ZeroconfDiscovery: _LOGGER.debug("Discovered new device %s %s", name, info) props: dict[str, str] = info.properties + domain = None # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES and ( @@ -458,10 +459,17 @@ class ZeroconfDiscovery: matcher_domain = matcher["domain"] assert isinstance(matcher_domain, str) + context = { + "source": config_entries.SOURCE_ZEROCONF, + } + if domain: + # Domain of integration that offers alternative API to handle this device. + context["alternative_domain"] = domain + discovery_flow.async_create_flow( self.hass, matcher_domain, - {"source": config_entries.SOURCE_ZEROCONF}, + context, info, ) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 6bc37e10da2..0de9929fcf8 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -327,6 +327,7 @@ async def test_zeroconf_match_macaddress(hass, mock_async_zeroconf): assert len(mock_service_browser.mock_calls) == 1 assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "shelly" + assert mock_config_flow.mock_calls[0][2]["context"] == {"source": "zeroconf"} async def test_zeroconf_match_manufacturer(hass, mock_async_zeroconf): @@ -533,6 +534,10 @@ async def test_homekit_match_partial_space(hass, mock_async_zeroconf): # One for HKC, and one for LIFX since lifx is local polling assert len(mock_config_flow.mock_calls) == 2 assert mock_config_flow.mock_calls[0][1][0] == "lifx" + assert mock_config_flow.mock_calls[1][2]["context"] == { + "source": "zeroconf", + "alternative_domain": "lifx", + } async def test_homekit_match_partial_dash(hass, mock_async_zeroconf): From adc5e9f2151f2fd21daa6b7a7c61d3e698735307 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 29 Sep 2022 18:07:26 -0700 Subject: [PATCH 031/183] Mask spotify content in owntone library (#79247) --- .../components/forked_daapd/browse_media.py | 4 +++ .../forked_daapd/test_browse_media.py | 28 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/forked_daapd/browse_media.py b/homeassistant/components/forked_daapd/browse_media.py index 88ca9ad60f8..099a042f58a 100644 --- a/homeassistant/components/forked_daapd/browse_media.py +++ b/homeassistant/components/forked_daapd/browse_media.py @@ -229,6 +229,10 @@ def create_browse_media_response( if not children: # Directory searches will pass in subdirectories as children children = [] for item in result: + if item.get("data_kind") == "spotify" or ( + "path" in item and cast(str, item["path"]).startswith("spotify") + ): # Exclude spotify data from Owntone library + continue assert isinstance(item["uri"], str) media_type = OWNTONE_TYPE_TO_MEDIA_TYPE[item["uri"].split(":")[1]] title = item.get("name") or item.get("title") # only tracks use title diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index ff26b6f9315..ba0473513b9 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -4,7 +4,11 @@ from http import HTTPStatus from unittest.mock import patch from homeassistant.components import media_source, spotify -from homeassistant.components.forked_daapd.browse_media import create_media_content_id +from homeassistant.components.forked_daapd.browse_media import ( + MediaContent, + create_media_content_id, + is_owntone_media_content_id, +) from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.components.spotify.const import ( MEDIA_PLAYER_PREFIX as SPOTIFY_MEDIA_PLAYER_PREFIX, @@ -111,6 +115,16 @@ async def test_async_browse_media(hass, hass_ws_client, config_entry): "length_ms": 2951554, "uri": "library:artist:3815427709949443149", }, + { + "id": "456", + "name": "Spotify Artist", + "name_sort": "Spotify Artist", + "album_count": 1, + "track_count": 10, + "length_ms": 2254, + "uri": "spotify:artist:abc123", + "data_kind": "spotify", + }, ] mock_api.return_value.get_genres.return_value = [ {"name": "Classical"}, @@ -127,6 +141,13 @@ async def test_async_browse_media(hass, hass_ws_client, config_entry): "smart_playlist": False, "uri": "library:playlist:1", }, + { + "id": 2, + "name": "Spotify Playlist", + "path": "spotify:playlist:abc123", + "smart_playlist": False, + "uri": "library:playlist:2", + }, ] # Request browse root through WebSocket @@ -150,6 +171,11 @@ async def test_async_browse_media(hass, hass_ws_client, config_entry): """Browse the children of this BrowseMedia.""" nonlocal msg_id for child in children: + # Assert Spotify content is not passed through as Owntone media + assert not ( + is_owntone_media_content_id(child["media_content_id"]) + and "Spotify" in MediaContent(child["media_content_id"]).title + ) if child["can_expand"]: await client.send_json( { From 54ba4a74bc46c688b93d87bf535b8725a2444154 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 30 Sep 2022 08:38:44 +0200 Subject: [PATCH 032/183] Improve naming of units used in statistics (#79276) --- homeassistant/components/recorder/core.py | 6 +- .../components/recorder/statistics.py | 12 +-- homeassistant/components/recorder/tasks.py | 6 +- .../components/recorder/websocket_api.py | 15 ++-- tests/components/demo/test_init.py | 4 +- tests/components/recorder/test_statistics.py | 6 +- .../components/recorder/test_websocket_api.py | 46 +++++------ tests/components/sensor/test_recorder.py | 76 +++++++++---------- 8 files changed, 88 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 17828a2e87e..c0f19f2e864 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -485,11 +485,13 @@ class Recorder(threading.Thread): statistic_id: str, start_time: datetime, sum_adjustment: float, - display_unit: str, + adjustment_unit: str, ) -> None: """Adjust statistics.""" self.queue_task( - AdjustStatisticsTask(statistic_id, start_time, sum_adjustment, display_unit) + AdjustStatisticsTask( + statistic_id, start_time, sum_adjustment, adjustment_unit + ) ) @callback diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 6a594827a5c..a0ff73b10fd 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -899,7 +899,7 @@ def list_statistic_ids( result = { meta["statistic_id"]: { - "display_unit_of_measurement": meta["state_unit_of_measurement"], + "state_unit_of_measurement": meta["state_unit_of_measurement"], "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], @@ -926,7 +926,7 @@ def list_statistic_ids( "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "display_unit_of_measurement": meta["state_unit_of_measurement"], + "state_unit_of_measurement": meta["state_unit_of_measurement"], "unit_class": _get_unit_class(meta["unit_of_measurement"]), "unit_of_measurement": meta["unit_of_measurement"], } @@ -939,7 +939,7 @@ def list_statistic_ids( "has_sum": info["has_sum"], "name": info.get("name"), "source": info["source"], - "display_unit_of_measurement": info["display_unit_of_measurement"], + "state_unit_of_measurement": info["state_unit_of_measurement"], "statistics_unit_of_measurement": info["unit_of_measurement"], "unit_class": info["unit_class"], } @@ -1605,7 +1605,7 @@ def adjust_statistics( statistic_id: str, start_time: datetime, sum_adjustment: float, - display_unit: str, + adjustment_unit: str, ) -> bool: """Process an add_statistics job.""" @@ -1617,7 +1617,9 @@ def adjust_statistics( return True statistic_unit = metadata[statistic_id][1]["unit_of_measurement"] - convert = _get_display_to_statistic_unit_converter(display_unit, statistic_unit) + convert = _get_display_to_statistic_unit_converter( + adjustment_unit, statistic_unit + ) sum_adjustment = convert(sum_adjustment) _adjust_sum_statistics( diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 63fb14cc598..4fa3a3cc40c 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -163,7 +163,7 @@ class AdjustStatisticsTask(RecorderTask): statistic_id: str start_time: datetime sum_adjustment: float - display_unit: str + adjustment_unit: str def run(self, instance: Recorder) -> None: """Run statistics task.""" @@ -172,7 +172,7 @@ class AdjustStatisticsTask(RecorderTask): self.statistic_id, self.start_time, self.sum_adjustment, - self.display_unit, + self.adjustment_unit, ): return # Schedule a new adjust statistics task if this one didn't finish @@ -181,7 +181,7 @@ class AdjustStatisticsTask(RecorderTask): self.statistic_id, self.start_time, self.sum_adjustment, - self.display_unit, + self.adjustment_unit, ) ) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index d841233fa5b..02b7519486d 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -291,7 +291,7 @@ def ws_change_statistics_unit( vol.Required("statistic_id"): str, vol.Required("start_time"): str, vol.Required("adjustment"): vol.Any(float, int), - vol.Required("display_unit"): vol.Any(str, None), + vol.Required("adjustment_unit_of_measurement"): vol.Any(str, None), } ) @websocket_api.async_response @@ -320,25 +320,26 @@ async def ws_adjust_sum_statistics( return metadata = metadatas[0] - def valid_units(statistics_unit: str | None, display_unit: str | None) -> bool: - if statistics_unit == display_unit: + def valid_units(statistics_unit: str | None, adjustment_unit: str | None) -> bool: + if statistics_unit == adjustment_unit: return True converter = STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistics_unit) - if converter is not None and display_unit in converter.VALID_UNITS: + if converter is not None and adjustment_unit in converter.VALID_UNITS: return True return False stat_unit = metadata["statistics_unit_of_measurement"] - if not valid_units(stat_unit, msg["display_unit"]): + adjustment_unit = msg["adjustment_unit_of_measurement"] + if not valid_units(stat_unit, adjustment_unit): connection.send_error( msg["id"], "invalid_units", - f"Can't convert {stat_unit} to {msg['display_unit']}", + f"Can't convert {stat_unit} to {adjustment_unit}", ) return get_instance(hass).async_adjust_statistics( - msg["statistic_id"], start_time, msg["adjustment"], msg["display_unit"] + msg["statistic_id"], start_time, msg["adjustment"], adjustment_unit ) connection.send_result(msg["id"]) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 934321a0ed8..8c3adeb1c98 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -63,21 +63,21 @@ async def test_demo_statistics(hass, recorder_mock): list_statistic_ids, hass ) assert { - "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": "Outdoor temperature", "source": "demo", + "state_unit_of_measurement": "°C", "statistic_id": "demo:temperature_outdoor", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", } in statistic_ids assert { - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": "Energy consumption 1", "source": "demo", + "state_unit_of_measurement": "kWh", "statistic_id": "demo:energy_consumption_kwh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 4fc98333bf4..c96b984bcf4 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -525,12 +525,12 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -621,12 +621,12 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { - "display_unit_of_measurement": "MWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy renamed", "source": source, + "state_unit_of_measurement": "MWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -682,7 +682,7 @@ async def test_import_statistics( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": "MWh", + "adjustment_unit_of_measurement": "MWh", } ) response = await client.receive_json() diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index e8d8093e131..4d4a1604a91 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -651,7 +651,7 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "display_unit_of_measurement": display_unit, + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -673,7 +673,7 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "display_unit_of_measurement": display_unit, + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -698,7 +698,7 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "display_unit_of_measurement": display_unit, + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -719,7 +719,7 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "display_unit_of_measurement": display_unit, + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -903,11 +903,11 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -931,11 +931,11 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": new_unit, "unit_class": new_unit_class, } @@ -995,11 +995,11 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -1051,11 +1051,11 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "W", "unit_class": "power", } @@ -1104,11 +1104,11 @@ async def test_change_statistics_unit_errors( expected_statistic_ids = [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -1483,11 +1483,11 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "test:total_gas", - "display_unit_of_measurement": unit, "has_mean": has_mean, "has_sum": has_sum, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": unit, "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1511,11 +1511,11 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": attributes["unit_of_measurement"], "has_mean": has_mean, "has_sum": has_sum, "name": None, "source": "recorder", + "state_unit_of_measurement": attributes["unit_of_measurement"], "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1539,11 +1539,11 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": attributes["unit_of_measurement"], "has_mean": has_mean, "has_sum": has_sum, "name": None, "source": "recorder", + "state_unit_of_measurement": attributes["unit_of_measurement"], "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1635,12 +1635,12 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1864,12 +1864,12 @@ async def test_adjust_sum_statistics_energy( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1898,7 +1898,7 @@ async def test_adjust_sum_statistics_energy( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": "kWh", + "adjustment_unit_of_measurement": "kWh", } ) response = await client.receive_json() @@ -1941,7 +1941,7 @@ async def test_adjust_sum_statistics_energy( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 2.0, - "display_unit": "MWh", + "adjustment_unit_of_measurement": "MWh", } ) response = await client.receive_json() @@ -2062,12 +2062,12 @@ async def test_adjust_sum_statistics_gas( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { - "display_unit_of_measurement": "m³", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "m³", "statistics_unit_of_measurement": "m³", "unit_class": "volume", } @@ -2096,7 +2096,7 @@ async def test_adjust_sum_statistics_gas( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": "m³", + "adjustment_unit_of_measurement": "m³", } ) response = await client.receive_json() @@ -2139,7 +2139,7 @@ async def test_adjust_sum_statistics_gas( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 35.3147, # ~1 m³ - "display_unit": "ft³", + "adjustment_unit_of_measurement": "ft³", } ) response = await client.receive_json() @@ -2276,12 +2276,12 @@ async def test_adjust_sum_statistics_errors( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { - "display_unit_of_measurement": state_unit, "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, } @@ -2311,7 +2311,7 @@ async def test_adjust_sum_statistics_errors( "statistic_id": "sensor.does_not_exist", "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": statistic_unit, + "adjustment_unit_of_measurement": statistic_unit, } ) response = await client.receive_json() @@ -2331,7 +2331,7 @@ async def test_adjust_sum_statistics_errors( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": unit, + "adjustment_unit_of_measurement": unit, } ) response = await client.receive_json() @@ -2351,7 +2351,7 @@ async def test_adjust_sum_statistics_errors( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": unit, + "adjustment_unit_of_measurement": unit, } ) response = await client.receive_json() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 9bddaa8af71..f0013874e23 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -136,11 +136,11 @@ def test_compile_hourly_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -210,12 +210,12 @@ def test_compile_hourly_statistics_purged_state_changes( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { - "display_unit_of_measurement": display_unit, "statistic_id": "sensor.test1", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -281,31 +281,31 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, { "statistic_id": "sensor.test6", - "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, { "statistic_id": "sensor.test7", - "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, @@ -436,11 +436,11 @@ async def test_compile_hourly_sum_statistics_amount( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -516,7 +516,7 @@ async def test_compile_hourly_sum_statistics_amount( "statistic_id": "sensor.test1", "start_time": period1.isoformat(), "adjustment": 100.0, - "display_unit": display_unit, + "adjustment_unit_of_measurement": display_unit, } ) response = await client.receive_json() @@ -536,7 +536,7 @@ async def test_compile_hourly_sum_statistics_amount( "statistic_id": "sensor.test1", "start_time": period2.isoformat(), "adjustment": -400.0, - "display_unit": display_unit, + "adjustment_unit_of_measurement": display_unit, } ) response = await client.receive_json() @@ -629,11 +629,11 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -730,11 +730,11 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -815,11 +815,11 @@ def test_compile_hourly_sum_statistics_nan_inf_state( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -929,11 +929,11 @@ def test_compile_hourly_sum_statistics_negative_state( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert { - "name": None, - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, + "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistic_id": entity_id, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, @@ -1018,11 +1018,11 @@ def test_compile_hourly_sum_statistics_total_no_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1121,11 +1121,11 @@ def test_compile_hourly_sum_statistics_total_increasing( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1235,11 +1235,11 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1330,11 +1330,11 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1423,31 +1423,31 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, { "statistic_id": "sensor.test2", - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, { "statistic_id": "sensor.test3", - "display_unit_of_measurement": "Wh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "Wh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, @@ -1807,11 +1807,11 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1822,11 +1822,11 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1913,11 +1913,11 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1949,11 +1949,11 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2025,11 +2025,11 @@ def test_compile_hourly_statistics_changing_units_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "cats", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "cats", "statistics_unit_of_measurement": "cats", "unit_class": unit_class, }, @@ -2091,11 +2091,11 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2127,11 +2127,11 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2193,11 +2193,11 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2239,11 +2239,11 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2302,11 +2302,11 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2382,11 +2382,11 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, }, @@ -2432,11 +2432,11 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, }, @@ -2502,11 +2502,11 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": None, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": None, "statistics_unit_of_measurement": None, "unit_class": None, }, @@ -2539,11 +2539,11 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": None, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": None, "statistics_unit_of_measurement": None, "unit_class": None, }, @@ -2734,41 +2734,41 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, { "statistic_id": "sensor.test2", - "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, { "statistic_id": "sensor.test3", - "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, { "statistic_id": "sensor.test4", - "display_unit_of_measurement": "EUR", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "EUR", "statistics_unit_of_measurement": "EUR", "unit_class": None, }, From ba8b01597f04b9c8b9d62c1a68ed06c0edc5bc6d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 29 Sep 2022 14:29:41 -0400 Subject: [PATCH 033/183] Unregister Google sheets services during unload (#79314) * Unregister services during unload - Google Sheets * uno mas --- homeassistant/components/google_sheets/__init__.py | 11 ++++++++++- tests/components/google_sheets/test_init.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index a4c10da7f23..ea96288371c 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -10,7 +10,7 @@ from google.oauth2.credentials import Credentials from gspread import Client import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -69,6 +69,15 @@ def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass.data[DOMAIN].pop(entry.entry_id) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + for service_name in hass.services.async_services()[DOMAIN]: + hass.services.async_remove(DOMAIN, service_name) + return True diff --git a/tests/components/google_sheets/test_init.py b/tests/components/google_sheets/test_init.py index d060e01bac2..c32eb345534 100644 --- a/tests/components/google_sheets/test_init.py +++ b/tests/components/google_sheets/test_init.py @@ -80,6 +80,7 @@ async def mock_setup_integration( assert len(entries) == 1 await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() + assert not len(hass.services.async_services().get(DOMAIN, {})) assert not hass.data.get(DOMAIN) assert entries[0].state is ConfigEntryState.NOT_LOADED From 5c86e47e99c720681e2c1db71e9947c1078d6759 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Sep 2022 08:37:31 -1000 Subject: [PATCH 034/183] Handle short local names from esphome proxies (#79321) --- .../components/esphome/bluetooth/scanner.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index fbd5f185907..36138192f8f 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -83,15 +83,23 @@ class ESPHomeScanner(BaseHaScanner): """Call the registered callback.""" now = time.monotonic() address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper + name = adv.name + if prev_discovery := self._discovered_devices.get(address): + # If the last discovery had the full local name + # and this one doesn't, keep the old one as we + # always want the full local name over the short one + if len(prev_discovery.name) > len(adv.name): + name = prev_discovery.name + advertisement_data = AdvertisementData( # type: ignore[no-untyped-call] - local_name=None if adv.name == "" else adv.name, + local_name=None if name == "" else name, manufacturer_data=adv.manufacturer_data, service_data=adv.service_data, service_uuids=adv.service_uuids, ) device = BLEDevice( # type: ignore[no-untyped-call] address=address, - name=adv.name, + name=name, details=self._details, rssi=adv.rssi, ) From ffb8be167fbf189545e3fde25d3564be31ae9423 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Fri, 30 Sep 2022 03:51:18 +0200 Subject: [PATCH 035/183] Use SensorDeviceClass.VOLUME in HomeWizard (#79323) Co-authored-by: Paulus Schoutsen --- homeassistant/components/homewizard/sensor.py | 2 +- tests/components/homewizard/test_sensor.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index a66a2664ae1..df4eb0c4880 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -130,7 +130,7 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( key="total_liter_m3", name="Total water usage", native_unit_of_measurement=VOLUME_CUBIC_METERS, - icon="mdi:gauge", + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, ), ) diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 145a2719b01..7d350764b2b 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -609,8 +609,8 @@ async def test_sensor_entity_total_liters( assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS - assert ATTR_DEVICE_CLASS not in state.attributes - assert state.attributes.get(ATTR_ICON) == "mdi:gauge" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME + assert state.attributes.get(ATTR_ICON) is None async def test_sensor_entity_disabled_when_null( From ffaa277f1809cc6c152b514612fa8c3c762526a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Sep 2022 02:29:36 -1000 Subject: [PATCH 036/183] Switch to using new esphome bluetooth_proxy_version field (#79331) --- homeassistant/components/esphome/__init__.py | 2 +- .../components/esphome/bluetooth/__init__.py | 14 ++++++++------ homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 90f1bac8de2..8846007374e 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -236,7 +236,7 @@ async def async_setup_entry( # noqa: C901 await cli.subscribe_states(entry_data.async_update_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states(async_on_state_subscription) - if entry_data.device_info.has_bluetooth_proxy: + if entry_data.device_info.bluetooth_proxy_version: entry_data.disconnect_callbacks.append( await async_connect_scanner(hass, entry, cli, entry_data) ) diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index 4061333b4f3..4f3235676a4 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging from aioesphomeapi import APIClient -from awesomeversion import AwesomeVersion from homeassistant.components.bluetooth import ( HaBluetoothConnector, @@ -24,7 +23,6 @@ from ..entry_data import RuntimeEntryData from .client import ESPHomeClient from .scanner import ESPHomeScanner -CONNECTABLE_MIN_VERSION = AwesomeVersion("2022.10.0-dev") _LOGGER = logging.getLogger(__name__) @@ -53,10 +51,14 @@ async def async_connect_scanner( assert entry.unique_id is not None source = str(entry.unique_id) new_info_callback = async_get_advertisement_callback(hass) - connectable = bool( - entry_data.device_info - and AwesomeVersion(entry_data.device_info.esphome_version) - >= CONNECTABLE_MIN_VERSION + assert entry_data.device_info is not None + version = entry_data.device_info.bluetooth_proxy_version + connectable = version >= 2 + _LOGGER.debug( + "Connecting scanner for %s, version=%s, connectable=%s", + source, + version, + connectable, ) connector = HaBluetoothConnector( client=ESPHomeClient, diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d094b8518ef..c6a475b6eea 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.14.0"], + "requirements": ["aioesphomeapi==11.0.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 74cce746d09..0d212b3470c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.14.0 +aioesphomeapi==11.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1d5a1f3618..e793a7ec5d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.14.0 +aioesphomeapi==11.0.0 # homeassistant.components.flo aioflo==2021.11.0 From dcae0006833c0846077dcfd042a3065ffc6db56f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Sep 2022 02:46:45 -1000 Subject: [PATCH 037/183] Remove iBeacon devices that rotate their major,minor and mac (#79338) --- homeassistant/components/ibeacon/const.py | 5 ++ .../components/ibeacon/coordinator.py | 42 +++++++++++- .../components/ibeacon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ibeacon/test_coordinator.py | 68 +++++++++++++++++++ 6 files changed, 117 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ibeacon/const.py b/homeassistant/components/ibeacon/const.py index 9b7a5a81dd3..7d1ab15da0a 100644 --- a/homeassistant/components/ibeacon/const.py +++ b/homeassistant/components/ibeacon/const.py @@ -27,4 +27,9 @@ UPDATE_INTERVAL = timedelta(seconds=60) # we will add it to the ignore list since its garbage data. MAX_IDS = 10 +# If a device broadcasts this many major minors for the same uuid +# we will add it to the ignore list since its garbage data. +MAX_IDS_PER_UUID = 50 + CONF_IGNORE_ADDRESSES = "ignore_addresses" +CONF_IGNORE_UUIDS = "ignore_uuids" diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 546b40c0c1b..2260624558e 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -23,8 +23,10 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_IGNORE_ADDRESSES, + CONF_IGNORE_UUIDS, DOMAIN, MAX_IDS, + MAX_IDS_PER_UUID, SIGNAL_IBEACON_DEVICE_NEW, SIGNAL_IBEACON_DEVICE_SEEN, SIGNAL_IBEACON_DEVICE_UNAVAILABLE, @@ -115,6 +117,9 @@ class IBeaconCoordinator: self._ignore_addresses: set[str] = set( entry.data.get(CONF_IGNORE_ADDRESSES, []) ) + # iBeacon devices that do not follow the spec + # and broadcast custom data in the major and minor fields + self._ignore_uuids: set[str] = set(entry.data.get(CONF_IGNORE_UUIDS, [])) # iBeacons with fixed MAC addresses self._last_ibeacon_advertisement_by_unique_id: dict[ @@ -131,6 +136,9 @@ class IBeaconCoordinator: self._last_seen_by_group_id: dict[str, bluetooth.BluetoothServiceInfoBleak] = {} self._unavailable_group_ids: set[str] = set() + # iBeacons with random MAC addresses, fixed UUID, random major/minor + self._major_minor_by_uuid: dict[str, set[tuple[int, int]]] = {} + @callback def _async_handle_unavailable( self, service_info: bluetooth.BluetoothServiceInfoBleak @@ -146,6 +154,25 @@ class IBeaconCoordinator: """Cancel unavailable tracking for an address.""" self._unavailable_trackers.pop(address)() + @callback + def _async_ignore_uuid(self, uuid: str) -> None: + """Ignore an UUID that does not follow the spec and any entities created by it.""" + self._ignore_uuids.add(uuid) + major_minor_by_uuid = self._major_minor_by_uuid.pop(uuid) + unique_ids_to_purge = set() + for major, minor in major_minor_by_uuid: + group_id = f"{uuid}_{major}_{minor}" + if unique_ids := self._unique_ids_by_group_id.pop(group_id, None): + unique_ids_to_purge.update(unique_ids) + for address in self._addresses_by_group_id.pop(group_id, []): + self._async_cancel_unavailable_tracker(address) + self._unique_ids_by_address.pop(address) + self._group_ids_by_address.pop(address) + self._async_purge_untrackable_entities(unique_ids_to_purge) + entry_data = self._entry.data + new_data = entry_data | {CONF_IGNORE_UUIDS: list(self._ignore_uuids)} + self.hass.config_entries.async_update_entry(self._entry, data=new_data) + @callback def _async_ignore_address(self, address: str) -> None: """Ignore an address that does not follow the spec and any entities created by it.""" @@ -203,7 +230,20 @@ class IBeaconCoordinator: return if not (ibeacon_advertisement := parse(service_info)): return - group_id = f"{ibeacon_advertisement.uuid}_{ibeacon_advertisement.major}_{ibeacon_advertisement.minor}" + + uuid_str = str(ibeacon_advertisement.uuid) + if uuid_str in self._ignore_uuids: + return + + major = ibeacon_advertisement.major + minor = ibeacon_advertisement.minor + major_minor_by_uuid = self._major_minor_by_uuid.setdefault(uuid_str, set()) + if len(major_minor_by_uuid) + 1 > MAX_IDS_PER_UUID: + self._async_ignore_uuid(uuid_str) + return + + major_minor_by_uuid.add((major, minor)) + group_id = f"{uuid_str}_{major}_{minor}" if group_id in self._group_ids_random_macs: self._async_update_ibeacon_with_random_mac( diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json index 9cecb399281..7b4110a7fe4 100644 --- a/homeassistant/components/ibeacon/manifest.json +++ b/homeassistant/components/ibeacon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ibeacon", "dependencies": ["bluetooth"], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], - "requirements": ["ibeacon_ble==0.7.1"], + "requirements": ["ibeacon_ble==0.7.2"], "codeowners": ["@bdraco"], "iot_class": "local_push", "loggers": ["bleak"], diff --git a/requirements_all.txt b/requirements_all.txt index 0d212b3470c..7b14429ff6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -898,7 +898,7 @@ iammeter==0.1.7 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.1 +ibeacon_ble==0.7.2 # homeassistant.components.watson_tts ibm-watson==5.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e793a7ec5d0..0aee1673634 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -669,7 +669,7 @@ hyperion-py==0.7.5 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.1 +ibeacon_ble==0.7.2 # homeassistant.components.ping icmplib==3.0 diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py index cb7e0bdefc8..5ea19914ee4 100644 --- a/tests/components/ibeacon/test_coordinator.py +++ b/tests/components/ibeacon/test_coordinator.py @@ -127,3 +127,71 @@ async def test_ignore_default_name(hass): ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == before_entity_count + + +async def test_rotating_major_minor_and_mac(hass): + """Test the different uuid, major, minor from many addresses removes all associated entities.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + before_entity_count = len(hass.states.async_entity_ids("device_tracker")) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + for i in range(100): + service_info = BluetoothServiceInfo( + name="BlueCharm_177999", + address=f"AA:BB:CC:DD:EE:{i:02X}", + rssi=-63, + service_data={}, + manufacturer_data={ + 76: b"\x02\x15BlueCharmBeacons" + + bytearray([i]) + + b"\xfe" + + bytearray([i]) + + b"U\xc5" + }, + service_uuids=[], + source="local", + ) + inject_bluetooth_service_info(hass, service_info) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("device_tracker")) == before_entity_count + + +async def test_rotating_major_minor_and_mac_no_name(hass): + """Test no-name devices with different uuid, major, minor from many addresses removes all associated entities.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + before_entity_count = len(hass.states.async_entity_ids("device_tracker")) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + for i in range(51): + service_info = BluetoothServiceInfo( + name=f"AA:BB:CC:DD:EE:{i:02X}", + address=f"AA:BB:CC:DD:EE:{i:02X}", + rssi=-63, + service_data={}, + manufacturer_data={ + 76: b"\x02\x15BlueCharmBeacons" + + bytearray([i]) + + b"\xfe" + + bytearray([i]) + + b"U\xc5" + }, + service_uuids=[], + source="local", + ) + inject_bluetooth_service_info(hass, service_info) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("device_tracker")) == before_entity_count From 3d4e5b88e08bc5f920ab795efe7880cb3f6e6381 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Sep 2022 22:33:14 -1000 Subject: [PATCH 038/183] Bump govee-ble to 0.19.1 to handle another H5181 (#79340) fixes #79188 --- homeassistant/components/govee_ble/manifest.json | 7 ++++++- homeassistant/generated/bluetooth.py | 6 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 29af7502ded..1cb1eeaddd8 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -37,6 +37,11 @@ "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 53579, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 43682, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", @@ -68,7 +73,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.19.0"], + "requirements": ["govee-ble==0.19.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 3036f691f00..8975ac66611 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -84,6 +84,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", "connectable": False, }, + { + "domain": "govee_ble", + "manufacturer_id": 53579, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": False, + }, { "domain": "govee_ble", "manufacturer_id": 43682, diff --git a/requirements_all.txt b/requirements_all.txt index 7b14429ff6a..e2164c59697 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.19.0 +govee-ble==0.19.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0aee1673634..543c4cce438 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -587,7 +587,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.19.0 +govee-ble==0.19.1 # homeassistant.components.gree greeclimate==1.3.0 From 5bab83511a23782c01409022b52ac705b071e3a9 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 29 Sep 2022 22:13:09 -0500 Subject: [PATCH 039/183] Add Third Reality to Zigbee Iot standards (#79341) --- homeassistant/brands/third_reality.json | 5 +++++ homeassistant/generated/integrations.json | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 homeassistant/brands/third_reality.json diff --git a/homeassistant/brands/third_reality.json b/homeassistant/brands/third_reality.json new file mode 100644 index 00000000000..172b74c42fc --- /dev/null +++ b/homeassistant/brands/third_reality.json @@ -0,0 +1,5 @@ +{ + "domain": "third_reality", + "name": "Third Reality", + "iot_standards": ["zigbee"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index dd40bb050fd..155e0b58ce7 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4412,6 +4412,12 @@ "iot_class": "local_polling", "name": "Thinking Cleaner" }, + "third_reality": { + "name": "Third Reality", + "iot_standards": [ + "zigbee" + ] + }, "thomson": { "config_flow": false, "iot_class": "local_polling", From 7e2af8685a053c5cce968b8be65f0842704989a5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:41:18 +0200 Subject: [PATCH 040/183] Adjust icons with new device classes (#79348) * Adjust icons with new device classes * Fix mysensors tests * Fix mysensors tests --- homeassistant/components/homewizard/sensor.py | 1 + homeassistant/components/litterrobot/sensor.py | 1 - homeassistant/components/mysensors/sensor.py | 2 -- tests/components/homewizard/test_sensor.py | 2 +- tests/components/mysensors/test_sensor.py | 3 ++- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index df4eb0c4880..6fc1d38ec12 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -130,6 +130,7 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( key="total_liter_m3", name="Total water usage", native_unit_of_measurement=VOLUME_CUBIC_METERS, + icon="mdi:gauge", device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, ), diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index b9d70528cab..1857931143d 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -109,7 +109,6 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { RobotSensorEntityDescription[LitterRobot4]( key="pet_weight", name="Pet weight", - icon="mdi:scale", native_unit_of_measurement=MASS_POUNDS, device_class=SensorDeviceClass.WEIGHT, ), diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 59c33a48884..f21d343f9c3 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -95,13 +95,11 @@ SENSORS: dict[str, SensorEntityDescription] = { key="V_WEIGHT", native_unit_of_measurement=MASS_KILOGRAMS, device_class=SensorDeviceClass.WEIGHT, - icon="mdi:weight-kilogram", ), "V_DISTANCE": SensorEntityDescription( key="V_DISTANCE", native_unit_of_measurement=LENGTH_METERS, device_class=SensorDeviceClass.DISTANCE, - icon="mdi:ruler", ), "V_IMPEDANCE": SensorEntityDescription( key="V_IMPEDANCE", diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 7d350764b2b..c8238c75643 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -610,7 +610,7 @@ async def test_sensor_entity_total_liters( assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME - assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_ICON) == "mdi:gauge" async def test_sensor_entity_disabled_when_null( diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 45fe98b98c7..58258682d5b 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -117,7 +117,8 @@ async def test_distance_sensor( assert state assert state.state == "15" - assert state.attributes[ATTR_ICON] == "mdi:ruler" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DISTANCE + assert ATTR_ICON not in state.attributes assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "cm" From a5b8ec7113afa68157a085d2c05b00bca1b6d525 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 30 Sep 2022 11:07:10 +0200 Subject: [PATCH 041/183] Make temperature conversions private (#79349) --- .../components/mold_indicator/sensor.py | 7 +++--- .../weather_update_coordinator.py | 5 +++- .../components/prometheus/__init__.py | 10 +++++--- homeassistant/util/unit_conversion.py | 24 +++++++++---------- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 23c5e639d7f..5685e76fac0 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -22,6 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util.unit_conversion import TemperatureConverter _LOGGER = logging.getLogger(__name__) @@ -218,7 +219,7 @@ class MoldIndicator(SensorEntity): # convert to celsius if necessary if unit == TEMP_FAHRENHEIT: - return util.temperature.fahrenheit_to_celsius(temp) + return TemperatureConverter.convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS) if unit == TEMP_CELSIUS: return temp _LOGGER.error( @@ -385,13 +386,13 @@ class MoldIndicator(SensorEntity): } dewpoint = ( - util.temperature.celsius_to_fahrenheit(self._dewpoint) + TemperatureConverter.convert(self._dewpoint, TEMP_CELSIUS, TEMP_FAHRENHEIT) if self._dewpoint is not None else None ) crit_temp = ( - util.temperature.celsius_to_fahrenheit(self._crit_temp) + TemperatureConverter.convert(self._crit_temp, TEMP_CELSIUS, TEMP_FAHRENHEIT) if self._crit_temp is not None else None ) diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index b5435c92680..630250b4701 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -9,6 +9,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, ) +from homeassistant.const import TEMP_CELSIUS, TEMP_KELVIN from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt @@ -191,7 +192,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _fmt_dewpoint(dewpoint): """Format the dewpoint data.""" if dewpoint is not None: - return round(TemperatureConverter.kelvin_to_celsius(dewpoint), 1) + return round( + TemperatureConverter.convert(dewpoint, TEMP_KELVIN, TEMP_CELSIUS), 1 + ) return None @staticmethod diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index e5dcebd3ca9..c1573755e11 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -348,7 +348,9 @@ class PrometheusMetrics: with suppress(ValueError): value = self.state_as_number(state) if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: - value = TemperatureConverter.fahrenheit_to_celsius(value) + value = TemperatureConverter.convert( + value, TEMP_FAHRENHEIT, TEMP_CELSIUS + ) metric.labels(**self._labels(state)).set(value) def _handle_device_tracker(self, state): @@ -394,7 +396,7 @@ class PrometheusMetrics: def _handle_climate_temp(self, state, attr, metric_name, metric_description): if temp := state.attributes.get(attr): if self._climate_units == TEMP_FAHRENHEIT: - temp = TemperatureConverter.fahrenheit_to_celsius(temp) + temp = TemperatureConverter.convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS) metric = self._metric( metric_name, self.prometheus_cli.Gauge, @@ -507,7 +509,9 @@ class PrometheusMetrics: try: value = self.state_as_number(state) if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: - value = TemperatureConverter.fahrenheit_to_celsius(value) + value = TemperatureConverter.convert( + value, TEMP_FAHRENHEIT, TEMP_CELSIUS + ) _metric.labels(**self._labels(state)).set(value) except ValueError: pass diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 84a42487498..cb066901b37 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -297,19 +297,19 @@ class TemperatureConverter(BaseUnitConverter): if from_unit == TEMP_CELSIUS: if to_unit == TEMP_FAHRENHEIT: - return cls.celsius_to_fahrenheit(value, interval) + return cls._celsius_to_fahrenheit(value, interval) if to_unit == TEMP_KELVIN: - return cls.celsius_to_kelvin(value, interval) + return cls._celsius_to_kelvin(value, interval) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) if from_unit == TEMP_FAHRENHEIT: if to_unit == TEMP_CELSIUS: - return cls.fahrenheit_to_celsius(value, interval) + return cls._fahrenheit_to_celsius(value, interval) if to_unit == TEMP_KELVIN: - return cls.celsius_to_kelvin( - cls.fahrenheit_to_celsius(value, interval), interval + return cls._celsius_to_kelvin( + cls._fahrenheit_to_celsius(value, interval), interval ) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) @@ -317,10 +317,10 @@ class TemperatureConverter(BaseUnitConverter): if from_unit == TEMP_KELVIN: if to_unit == TEMP_CELSIUS: - return cls.kelvin_to_celsius(value, interval) + return cls._kelvin_to_celsius(value, interval) if to_unit == TEMP_FAHRENHEIT: - return cls.celsius_to_fahrenheit( - cls.kelvin_to_celsius(value, interval), interval + return cls._celsius_to_fahrenheit( + cls._kelvin_to_celsius(value, interval), interval ) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) @@ -330,28 +330,28 @@ class TemperatureConverter(BaseUnitConverter): ) @classmethod - def fahrenheit_to_celsius(cls, fahrenheit: float, interval: bool = False) -> float: + def _fahrenheit_to_celsius(cls, fahrenheit: float, interval: bool = False) -> float: """Convert a temperature in Fahrenheit to Celsius.""" if interval: return fahrenheit / 1.8 return (fahrenheit - 32.0) / 1.8 @classmethod - def kelvin_to_celsius(cls, kelvin: float, interval: bool = False) -> float: + def _kelvin_to_celsius(cls, kelvin: float, interval: bool = False) -> float: """Convert a temperature in Kelvin to Celsius.""" if interval: return kelvin return kelvin - 273.15 @classmethod - def celsius_to_fahrenheit(cls, celsius: float, interval: bool = False) -> float: + def _celsius_to_fahrenheit(cls, celsius: float, interval: bool = False) -> float: """Convert a temperature in Celsius to Fahrenheit.""" if interval: return celsius * 1.8 return celsius * 1.8 + 32.0 @classmethod - def celsius_to_kelvin(cls, celsius: float, interval: bool = False) -> float: + def _celsius_to_kelvin(cls, celsius: float, interval: bool = False) -> float: """Convert a temperature in Celsius to Kelvin.""" if interval: return celsius From ac8805601d47c63de3e66536d710e17a155172e8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 30 Sep 2022 20:38:11 +0200 Subject: [PATCH 042/183] Realign util constants with 2022.9.7 (#79357) --- homeassistant/util/distance.py | 24 ++++++++++++++++++++++++ homeassistant/util/speed.py | 2 +- homeassistant/util/volume.py | 2 -- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 09cd55a9cee..f5dbeaf42d5 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -1,6 +1,8 @@ """Distance util functions.""" from __future__ import annotations +from collections.abc import Callable + from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 LENGTH, LENGTH_CENTIMETERS, @@ -19,6 +21,28 @@ from .unit_conversion import DistanceConverter VALID_UNITS = DistanceConverter.VALID_UNITS +TO_METERS: dict[str, Callable[[float], float]] = { + LENGTH_METERS: lambda meters: meters, + LENGTH_MILES: lambda miles: miles * 1609.344, + LENGTH_YARD: lambda yards: yards * 0.9144, + LENGTH_FEET: lambda feet: feet * 0.3048, + LENGTH_INCHES: lambda inches: inches * 0.0254, + LENGTH_KILOMETERS: lambda kilometers: kilometers * 1000, + LENGTH_CENTIMETERS: lambda centimeters: centimeters * 0.01, + LENGTH_MILLIMETERS: lambda millimeters: millimeters * 0.001, +} + +METERS_TO: dict[str, Callable[[float], float]] = { + LENGTH_METERS: lambda meters: meters, + LENGTH_MILES: lambda meters: meters * 0.000621371, + LENGTH_YARD: lambda meters: meters * 1.09361, + LENGTH_FEET: lambda meters: meters * 3.28084, + LENGTH_INCHES: lambda meters: meters * 39.3701, + LENGTH_KILOMETERS: lambda meters: meters * 0.001, + LENGTH_CENTIMETERS: lambda meters: meters * 100, + LENGTH_MILLIMETERS: lambda meters: meters * 1000, +} + def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index 18410272a7f..76ea873d7fe 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -26,7 +26,7 @@ from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401 ) # pylint: disable-next=protected-access -UNIT_CONVERSION = SpeedConverter._UNIT_CONVERSION +UNIT_CONVERSION: dict[str, float] = SpeedConverter._UNIT_CONVERSION VALID_UNITS = SpeedConverter.VALID_UNITS diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index f63f7de2cf3..b468b9e6e0d 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -15,8 +15,6 @@ from homeassistant.helpers.frame import report from .unit_conversion import VolumeConverter -# pylint: disable-next=protected-access -UNIT_CONVERSION = VolumeConverter._UNIT_CONVERSION VALID_UNITS = VolumeConverter.VALID_UNITS From 2436e375bbace0fcbd42f897a4cacac43c3ccea2 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 30 Sep 2022 18:41:38 +0200 Subject: [PATCH 043/183] Fjaraskupan stop on 0 percentage (#79367) * Make sure fan turns off on 0 percentage * Remember old percentage --- homeassistant/components/fjaraskupan/fan.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index c037966f0ef..c856a94fa07 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -82,11 +82,18 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): async def async_set_percentage(self, percentage: int) -> None: """Set speed.""" - new_speed = percentage_to_ordered_list_item( - ORDERED_NAMED_FAN_SPEEDS, percentage - ) + + # Proactively update percentage to mange successive increases + self._percentage = percentage + async with self.coordinator.async_connect_and_update() as device: - await device.send_fan_speed(int(new_speed)) + if percentage == 0: + await device.send_command(COMMAND_STOP_FAN) + else: + new_speed = percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ) + await device.send_fan_speed(int(new_speed)) async def async_turn_on( self, From 4db4ab1ab064dbdfb94e4e096e78a553b41beab8 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 30 Sep 2022 21:37:58 +0300 Subject: [PATCH 044/183] Make Shelly update sensors disabled by default (#79376) --- homeassistant/components/shelly/update.py | 4 ++-- tests/components/shelly/test_update.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index fa37b394b6c..ac4b737a2cc 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -70,7 +70,7 @@ REST_UPDATES: Final = { install=lambda wrapper: wrapper.async_trigger_ota_update(), device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=True, + entity_registry_enabled_default=False, ), "fwupdate_beta": RestUpdateDescription( name="Beta Firmware Update", @@ -94,7 +94,7 @@ RPC_UPDATES: Final = { install=lambda wrapper: wrapper.async_trigger_ota_update(), device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=True, + entity_registry_enabled_default=False, ), "fwupdate_beta": RpcUpdateDescription( name="Beta Firmware Update", diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 70c9b7a8e67..4d863c59390 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -1,7 +1,6 @@ """Tests for Shelly update platform.""" from homeassistant.components.shelly.const import DOMAIN -from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN -from homeassistant.components.update.const import SERVICE_INSTALL +from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity @@ -16,8 +15,8 @@ async def test_block_update(hass: HomeAssistant, coap_wrapper, monkeypatch): entity_registry.async_get_or_create( UPDATE_DOMAIN, DOMAIN, - "test_name_update", - suggested_object_id="test_name_update", + "test-mac-fwupdate", + suggested_object_id="test_name_firmware_update", disabled_by=None, ) hass.async_create_task( @@ -62,8 +61,8 @@ async def test_rpc_update(hass: HomeAssistant, rpc_wrapper, monkeypatch): entity_registry.async_get_or_create( UPDATE_DOMAIN, DOMAIN, - "test_name_update", - suggested_object_id="test_name_update", + "12345678-sys-fwupdate", + suggested_object_id="test_name_firmware_update", disabled_by=None, ) From 62679950b1c3e73ea183c0ce13b44732d61ffb8a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Sep 2022 14:47:22 -0400 Subject: [PATCH 045/183] Bumped version to 2022.10.0b2 --- 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 3d1dce999b0..8b5ad8f9666 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 = 10 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __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 e6037e4734c..294e90fecf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0b1" +version = "2022.10.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1836dc4a3b898ba49ed6fbbb8482dd137d00f449 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 1 Oct 2022 17:42:11 -0700 Subject: [PATCH 046/183] Address Google Sheets PR feedback (#78889) --- .../components/google_sheets/__init__.py | 12 +-- .../components/google_sheets/config_flow.py | 35 +++---- .../components/google_sheets/strings.json | 4 + .../google_sheets/test_config_flow.py | 63 ++++++++++++ tests/components/google_sheets/test_init.py | 96 ++++++++++++++++--- 5 files changed, 172 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index ea96288371c..e211693bf21 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -from typing import cast import aiohttp from google.auth.exceptions import RefreshError @@ -105,12 +104,13 @@ async def async_setup_service(hass: HomeAssistant) -> None: async def append_to_sheet(call: ServiceCall) -> None: """Append new line of data to a Google Sheets document.""" - - entry = cast( - ConfigEntry, - hass.config_entries.async_get_entry(call.data[DATA_CONFIG_ENTRY]), + entry: ConfigEntry | None = hass.config_entries.async_get_entry( + call.data[DATA_CONFIG_ENTRY] ) - session: OAuth2Session = hass.data[DOMAIN][entry.entry_id] + if not entry: + raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}") + if not (session := hass.data[DOMAIN].get(entry.entry_id)): + raise ValueError(f"Config entry not loaded: {call.data[DATA_CONFIG_ENTRY]}") await session.async_ensure_token_valid() await hass.async_add_executor_job(_append_to_sheet, call, entry) diff --git a/homeassistant/components/google_sheets/config_flow.py b/homeassistant/components/google_sheets/config_flow.py index d19a5b5c3fa..3805ee9d38b 100644 --- a/homeassistant/components/google_sheets/config_flow.py +++ b/homeassistant/components/google_sheets/config_flow.py @@ -8,7 +8,7 @@ from typing import Any from google.oauth2.credentials import Credentials from gspread import Client, GSpreadException -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow @@ -25,6 +25,8 @@ class OAuth2FlowHandler( DOMAIN = DOMAIN + reauth_entry: ConfigEntry | None = None + @property def logger(self) -> logging.Logger: """Return logger.""" @@ -42,6 +44,9 @@ class OAuth2FlowHandler( async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -52,40 +57,27 @@ class OAuth2FlowHandler( return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() - def _async_reauth_entry(self) -> ConfigEntry | None: - """Return existing entry for reauth.""" - if self.source != SOURCE_REAUTH or not ( - entry_id := self.context.get("entry_id") - ): - return None - return next( - ( - entry - for entry in self._async_current_entries() - if entry.entry_id == entry_id - ), - None, - ) - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow, or update existing entry.""" service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])) - if entry := self._async_reauth_entry(): + if self.reauth_entry: _LOGGER.debug("service.open_by_key") try: await self.hass.async_add_executor_job( service.open_by_key, - entry.unique_id, + self.reauth_entry.unique_id, ) except GSpreadException as err: _LOGGER.error( - "Could not find spreadsheet '%s': %s", entry.unique_id, str(err) + "Could not find spreadsheet '%s': %s", + self.reauth_entry.unique_id, + str(err), ) return self.async_abort(reason="open_spreadsheet_failure") - self.hass.config_entries.async_update_entry(entry, data=data) - await self.hass.config_entries.async_reload(entry.entry_id) + self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") try: @@ -97,6 +89,7 @@ class OAuth2FlowHandler( return self.async_abort(reason="create_spreadsheet_failure") await self.async_set_unique_id(doc.id) + self._abort_if_unique_id_configured() return self.async_create_entry( title=DEFAULT_NAME, data=data, description_placeholders={"url": doc.url} ) diff --git a/homeassistant/components/google_sheets/strings.json b/homeassistant/components/google_sheets/strings.json index 2170f6e4c1d..33230038cdf 100644 --- a/homeassistant/components/google_sheets/strings.json +++ b/homeassistant/components/google_sheets/strings.json @@ -10,6 +10,10 @@ }, "auth": { "title": "Link Google Account" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Google Sheets integration needs to re-authenticate your account" } }, "abort": { diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index 3fcd2f99ed0..e74602dc8a1 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -312,3 +312,66 @@ async def test_reauth_abort( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("type") == "abort" assert result.get("reason") == "open_spreadsheet_failure" + + +async def test_already_configured( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, + mock_client, +) -> None: + """Test case where config flow discovers unique id was already configured.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=SHEET_ID, + data={ + "token": { + "access_token": "mock-access-token", + }, + }, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + "google_sheets", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + # Prepare fake client library response when creating the sheet + mock_create = Mock() + mock_create.return_value.id = SHEET_ID + mock_client.return_value.create = mock_create + + aioclient_mock.post( + oauth2client.GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" diff --git a/tests/components/google_sheets/test_init.py b/tests/components/google_sheets/test_init.py index c32eb345534..f77edcbb491 100644 --- a/tests/components/google_sheets/test_init.py +++ b/tests/components/google_sheets/test_init.py @@ -14,6 +14,7 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.google_sheets import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceNotFound from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -75,16 +76,6 @@ async def mock_setup_integration( yield func - # Verify clean unload - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - await hass.config_entries.async_unload(entries[0].entry_id) - await hass.async_block_till_done() - assert not len(hass.services.async_services().get(DOMAIN, {})) - - assert not hass.data.get(DOMAIN) - assert entries[0].state is ConfigEntryState.NOT_LOADED - async def test_setup_success( hass: HomeAssistant, setup_integration: ComponentSetup @@ -96,6 +87,13 @@ async def test_setup_success( assert len(entries) == 1 assert entries[0].state is ConfigEntryState.LOADED + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert entries[0].state is ConfigEntryState.NOT_LOADED + assert not len(hass.services.async_services().get(DOMAIN, {})) + @pytest.mark.parametrize( "scopes", @@ -194,7 +192,7 @@ async def test_append_sheet( setup_integration: ComponentSetup, config_entry: MockConfigEntry, ) -> None: - """Test successful setup and unload.""" + """Test service call appending to a sheet.""" await setup_integration() entries = hass.config_entries.async_entries(DOMAIN) @@ -213,3 +211,79 @@ async def test_append_sheet( blocking=True, ) assert len(mock_client.mock_calls) == 8 + + +async def test_append_sheet_invalid_config_entry( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + expires_at: int, + scopes: list[str], +) -> None: + """Test service call with invalid config entries.""" + config_entry2 = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_SHEET_ID + "2", + data={ + "auth_implementation": DOMAIN, + "token": { + "access_token": "mock-access-token", + "refresh_token": "mock-refresh-token", + "expires_at": expires_at, + "scope": " ".join(scopes), + }, + }, + ) + config_entry2.add_to_hass(hass) + + await setup_integration() + + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry2.state is ConfigEntryState.LOADED + + # Exercise service call on a config entry that does not exist + with pytest.raises(ValueError, match="Invalid config entry"): + await hass.services.async_call( + DOMAIN, + "append_sheet", + { + "config_entry": config_entry.entry_id + "XXX", + "worksheet": "Sheet1", + "data": {"foo": "bar"}, + }, + blocking=True, + ) + + # Unload the config entry invoke the service on the unloaded entry id + await hass.config_entries.async_unload(config_entry2.entry_id) + await hass.async_block_till_done() + assert config_entry2.state is ConfigEntryState.NOT_LOADED + + with pytest.raises(ValueError, match="Config entry not loaded"): + await hass.services.async_call( + DOMAIN, + "append_sheet", + { + "config_entry": config_entry2.entry_id, + "worksheet": "Sheet1", + "data": {"foo": "bar"}, + }, + blocking=True, + ) + + # Unloading the other config entry will de-register the service + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED + + with pytest.raises(ServiceNotFound): + await hass.services.async_call( + DOMAIN, + "append_sheet", + { + "config_entry": config_entry.entry_id, + "worksheet": "Sheet1", + "data": {"foo": "bar"}, + }, + blocking=True, + ) From 2e2d8d13675be9c41125fe2010dfd4ffc5072cf3 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Sun, 2 Oct 2022 11:16:16 +1100 Subject: [PATCH 047/183] Powerview bump aiopvapi to 2.0.2 (#79274) --- homeassistant/components/hunterdouglas_powerview/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 2dbec2aba1c..930e80733e0 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,7 +2,7 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==2.0.1"], + "requirements": ["aiopvapi==2.0.2"], "codeowners": ["@bdraco", "@kingy444", "@trullock"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index e2164c59697..344b76a9283 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -229,7 +229,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.1 +aiopvapi==2.0.2 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 543c4cce438..de1ab3b2738 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,7 +204,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.1 +aiopvapi==2.0.2 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 From 783f514df39df1003186e93670f6b1cb7bdf257c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 14:17:45 -1000 Subject: [PATCH 048/183] Enable delete device support for iBeacon (#79339) --- homeassistant/components/ibeacon/__init__.py | 14 +++- .../components/ibeacon/coordinator.py | 8 +++ tests/components/ibeacon/test_init.py | 70 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/components/ibeacon/test_init.py diff --git a/homeassistant/components/ibeacon/__init__.py b/homeassistant/components/ibeacon/__init__.py index bf618c4ca12..2c191211910 100644 --- a/homeassistant/components/ibeacon/__init__.py +++ b/homeassistant/components/ibeacon/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get +from homeassistant.helpers.device_registry import DeviceEntry, async_get from .const import DOMAIN, PLATFORMS from .coordinator import IBeaconCoordinator @@ -22,3 +22,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data.pop(DOMAIN) return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove iBeacon config entry from a device.""" + coordinator: IBeaconCoordinator = hass.data[DOMAIN] + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN and coordinator.async_device_id_seen(identifier[1]) + ) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 2260624558e..9979cdf4fa8 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -139,6 +139,14 @@ class IBeaconCoordinator: # iBeacons with random MAC addresses, fixed UUID, random major/minor self._major_minor_by_uuid: dict[str, set[tuple[int, int]]] = {} + @callback + def async_device_id_seen(self, device_id: str) -> bool: + """Return True if the device_id has been seen since boot.""" + return bool( + device_id in self._last_ibeacon_advertisement_by_unique_id + or device_id in self._last_seen_by_group_id + ) + @callback def _async_handle_unavailable( self, service_info: bluetooth.BluetoothServiceInfoBleak diff --git a/tests/components/ibeacon/test_init.py b/tests/components/ibeacon/test_init.py new file mode 100644 index 00000000000..a04799e3cd4 --- /dev/null +++ b/tests/components/ibeacon/test_init.py @@ -0,0 +1,70 @@ +"""Test the ibeacon init.""" + +import pytest + +from homeassistant.components.ibeacon.const import DOMAIN +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from . import BLUECHARM_BEACON_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + +async def remove_device(ws_client, device_id, config_entry_id): + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] + + +async def test_device_remove_devices(hass, hass_ws_client): + """Test we can only remove a device that no longer exists.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, "config", {}) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + inject_bluetooth_service_info(hass, BLUECHARM_BEACON_SERVICE_INFO) + await hass.async_block_till_done() + device_registry = dr.async_get(hass) + + device_entry = device_registry.async_get_device( + { + ( + DOMAIN, + "426c7565-4368-6172-6d42-6561636f6e73_3838_4949_61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + }, + {}, + ) + assert ( + await remove_device(await hass_ws_client(hass), device_entry.id, entry.entry_id) + is False + ) + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "not_seen")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), dead_device_entry.id, entry.entry_id + ) + is True + ) From ecb934ee6accb0ab73b9d0b55711f7b955de14fb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 1 Oct 2022 09:52:07 +0200 Subject: [PATCH 049/183] Fix _attr_name issue in Yale Smart Alarm (#79378) Fix name issue --- .../components/yale_smart_alarm/alarm_control_panel.py | 3 ++- homeassistant/components/yale_smart_alarm/lock.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index e2df1b09ebe..8577a2a179f 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -14,6 +14,7 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -80,7 +81,7 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): ) except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not set alarm for {self._attr_name}: {error}" + f"Could not set alarm for {self.coordinator.entry.data[CONF_NAME]}: {error}" ) from error if alarm_state: diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index 8f9ed6c9ce1..807ecdecada 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -46,7 +46,7 @@ class YaleDoorlock(YaleEntity, LockEntity): """Initialize the Yale Lock Device.""" super().__init__(coordinator, data) self._attr_code_format = f"^\\d{code_format}$" - self.lock_name = data["name"] + self.lock_name: str = data["name"] async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" @@ -79,14 +79,14 @@ class YaleDoorlock(YaleEntity, LockEntity): ) except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not set lock for {self._attr_name}: {error}" + f"Could not set lock for {self.lock_name}: {error}" ) from error if lock_state: self.coordinator.data["lock_map"][self._attr_unique_id] = command self.async_write_ha_state() return - raise HomeAssistantError("Could set lock, check system ready for lock.") + raise HomeAssistantError("Could not set lock, check system ready for lock.") @property def is_locked(self) -> bool | None: From be036914a66d78d07b6243c5c40cd83f95eea1a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 04:44:45 -1000 Subject: [PATCH 050/183] Improve robustness of linking homekit yaml to config entries (#79386) --- homeassistant/components/homekit/__init__.py | 71 +++++++++++------- tests/components/homekit/test_homekit.py | 78 +++++++++++++++++++- 2 files changed, 120 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ee53c6da665..4859d9e1065 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -193,14 +193,21 @@ def _async_all_homekit_instances(hass: HomeAssistant) -> list[HomeKit]: ] -def _async_get_entries_by_name( +def _async_get_imported_entries_indices( current_entries: list[ConfigEntry], -) -> dict[str, ConfigEntry]: - """Return a dict of the entries by name.""" +) -> tuple[dict[str, ConfigEntry], dict[int, ConfigEntry]]: + """Return a dicts of the entries by name and port.""" # For backwards compat, its possible the first bridge is using the default # name. - return {entry.data.get(CONF_NAME, BRIDGE_NAME): entry for entry in current_entries} + entries_by_name: dict[str, ConfigEntry] = {} + entries_by_port: dict[int, ConfigEntry] = {} + for entry in current_entries: + if entry.source != SOURCE_IMPORT: + continue + entries_by_name[entry.data.get(CONF_NAME, BRIDGE_NAME)] = entry + entries_by_port[entry.data.get(CONF_PORT, DEFAULT_PORT)] = entry + return entries_by_name, entries_by_port async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -218,10 +225,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True current_entries = hass.config_entries.async_entries(DOMAIN) - entries_by_name = _async_get_entries_by_name(current_entries) + entries_by_name, entries_by_port = _async_get_imported_entries_indices( + current_entries + ) for index, conf in enumerate(config[DOMAIN]): - if _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): + if _async_update_config_entry_from_yaml( + hass, entries_by_name, entries_by_port, conf + ): continue conf[CONF_ENTRY_INDEX] = index @@ -237,8 +248,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback -def _async_update_config_entry_if_from_yaml( - hass: HomeAssistant, entries_by_name: dict[str, ConfigEntry], conf: ConfigType +def _async_update_config_entry_from_yaml( + hass: HomeAssistant, + entries_by_name: dict[str, ConfigEntry], + entries_by_port: dict[int, ConfigEntry], + conf: ConfigType, ) -> bool: """Update a config entry with the latest yaml. @@ -246,27 +260,24 @@ def _async_update_config_entry_if_from_yaml( Returns False if there is no matching config entry """ - bridge_name = conf[CONF_NAME] - - if ( - bridge_name in entries_by_name - and entries_by_name[bridge_name].source == SOURCE_IMPORT + if not ( + matching_entry := entries_by_name.get(conf.get(CONF_NAME, BRIDGE_NAME)) + or entries_by_port.get(conf.get(CONF_PORT, DEFAULT_PORT)) ): - entry = entries_by_name[bridge_name] - # If they alter the yaml config we import the changes - # since there currently is no practical way to support - # all the options in the UI at this time. - data = conf.copy() - options = {} - for key in CONFIG_OPTIONS: - if key in data: - options[key] = data[key] - del data[key] + return False - hass.config_entries.async_update_entry(entry, data=data, options=options) - return True + # If they alter the yaml config we import the changes + # since there currently is no practical way to support + # all the options in the UI at this time. + data = conf.copy() + options = {} + for key in CONFIG_OPTIONS: + if key in data: + options[key] = data[key] + del data[key] - return False + hass.config_entries.async_update_entry(matching_entry, data=data, options=options) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -451,10 +462,14 @@ def _async_register_events_and_services(hass: HomeAssistant) -> None: return current_entries = hass.config_entries.async_entries(DOMAIN) - entries_by_name = _async_get_entries_by_name(current_entries) + entries_by_name, entries_by_port = _async_get_imported_entries_indices( + current_entries + ) for conf in config[DOMAIN]: - _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf) + _async_update_config_entry_from_yaml( + hass, entries_by_name, entries_by_port, conf + ) reload_tasks = [ hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index be514ce2b6a..dbb63ba690a 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -35,7 +35,7 @@ from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_ZEROCONF from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_DEVICE_ID, @@ -1394,6 +1394,82 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_async_zeroco mock_homekit().async_start.assert_called() +async def test_yaml_can_link_with_default_name(hass, mock_async_zeroconf): + """Test async_setup with imported config linked by default name.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={}, + options={}, + ) + entry.add_to_hass(hass) + + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ): + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component( + hass, + "homekit", + {"homekit": {"entity_config": {"camera.back_camera": {"stream_count": 3}}}}, + ) + await hass.async_block_till_done() + + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert entry.options["entity_config"]["camera.back_camera"]["stream_count"] == 3 + + +async def test_yaml_can_link_with_port(hass, mock_async_zeroconf): + """Test async_setup with imported config linked by port.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={"name": "random", "port": 12345}, + options={}, + ) + entry.add_to_hass(hass) + entry2 = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={"name": "random", "port": 12346}, + options={}, + ) + entry2.add_to_hass(hass) + entry3 = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_ZEROCONF, + data={"name": "random", "port": 12347}, + options={}, + ) + entry3.add_to_hass(hass) + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ): + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component( + hass, + "homekit", + { + "homekit": { + "port": 12345, + "entity_config": {"camera.back_camera": {"stream_count": 3}}, + } + }, + ) + await hass.async_block_till_done() + + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert entry.options["entity_config"]["camera.back_camera"]["stream_count"] == 3 + assert entry2.options == {} + assert entry3.options == {} + + async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_async_zeroconf): """Test HomeKit uses system zeroconf.""" entry = MockConfigEntry( From a0ba102492c77baf0b1dbeddad2b4402b1fb90fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 14:42:54 -1000 Subject: [PATCH 051/183] Ensure bluetooth disconnect callback fires if esphome config entry is reloaded (#79389) --- .../components/esphome/bluetooth/client.py | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 8e8d7cf6427..14924756074 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -15,10 +15,9 @@ from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakError -from homeassistant.core import CALLBACK_TYPE, async_get_hass, callback as hass_callback +from homeassistant.core import CALLBACK_TYPE, async_get_hass from ..domain_data import DomainData -from ..entry_data import RuntimeEntryData from .characteristic import BleakGATTCharacteristicESPHome from .descriptor import BleakGATTDescriptorESPHome from .service import BleakGATTServiceESPHome @@ -85,7 +84,9 @@ class ESPHomeClient(BaseBleakClient): assert self._ble_device.details is not None self._source = self._ble_device.details["source"] self.domain_data = DomainData.get(async_get_hass()) - self._client = self._async_get_entry_data().client + config_entry = self.domain_data.get_by_unique_id(self._source) + self.entry_data = self.domain_data.get_entry_data(config_entry) + self._client = self.entry_data.client self._is_connected = False self._mtu: int | None = None self._cancel_connection_state: CALLBACK_TYPE | None = None @@ -108,12 +109,6 @@ class ESPHomeClient(BaseBleakClient): ) self._cancel_connection_state = None - @hass_callback - def _async_get_entry_data(self) -> RuntimeEntryData: - """Get the entry data.""" - config_entry = self.domain_data.get_by_unique_id(self._source) - return self.domain_data.get_entry_data(config_entry) - def _async_ble_device_disconnected(self) -> None: """Handle the BLE device disconnecting from the ESP.""" _LOGGER.debug("%s: BLE device disconnected", self._source) @@ -125,8 +120,7 @@ class ESPHomeClient(BaseBleakClient): def _async_esp_disconnected(self) -> None: """Handle the esp32 client disconnecting from hass.""" _LOGGER.debug("%s: ESP device disconnected", self._source) - entry_data = self._async_get_entry_data() - entry_data.disconnect_callbacks.remove(self._async_esp_disconnected) + self.entry_data.disconnect_callbacks.remove(self._async_esp_disconnected) self._async_ble_device_disconnected() def _async_call_bleak_disconnected_callback(self) -> None: @@ -179,8 +173,7 @@ class ESPHomeClient(BaseBleakClient): connected_future.set_exception(BleakError("Disconnected")) return - entry_data = self._async_get_entry_data() - entry_data.disconnect_callbacks.append(self._async_esp_disconnected) + self.entry_data.disconnect_callbacks.append(self._async_esp_disconnected) connected_future.set_result(connected) timeout = kwargs.get("timeout", self._timeout) @@ -203,14 +196,13 @@ class ESPHomeClient(BaseBleakClient): async def _wait_for_free_connection_slot(self, timeout: float) -> None: """Wait for a free connection slot.""" - entry_data = self._async_get_entry_data() - if entry_data.ble_connections_free: + if self.entry_data.ble_connections_free: return _LOGGER.debug( "%s: Out of connection slots, waiting for a free one", self._source ) async with async_timeout.timeout(timeout): - await entry_data.wait_for_ble_connections_free() + await self.entry_data.wait_for_ble_connections_free() @property def is_connected(self) -> bool: From ebb213eb14d438af2387f244d2b985d5ef192460 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 1 Oct 2022 09:28:15 -0700 Subject: [PATCH 052/183] Fix onvif snapshot fallback (#79394) Co-authored-by: Franck Nijhof --- homeassistant/components/onvif/camera.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 6c76f98a8da..9a8535f2599 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -142,16 +142,21 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): if self.device.capabilities.snapshot: try: - image = await self.device.device.get_snapshot( + if image := await self.device.device.get_snapshot( self.profile.token, self._basic_auth - ) - return image + ): + return image except ONVIFError as err: LOGGER.error( "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", self.device.name, err, ) + else: + LOGGER.error( + "Fetch snapshot image failed from %s, falling back to FFmpeg", + self.device.name, + ) assert self._stream_uri return await ffmpeg.async_get_image( From b3c43b981adcd7ced60b6904fe98a2ac408640cb Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 1 Oct 2022 14:33:11 +0000 Subject: [PATCH 053/183] Do not use AQI device class for CAQI sensor in Airly integration (#79402) --- homeassistant/components/airly/sensor.py | 2 +- tests/components/airly/test_sensor.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index bbb501ae47b..122990adecc 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -68,7 +68,7 @@ class AirlySensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( AirlySensorEntityDescription( key=ATTR_API_CAQI, - device_class=SensorDeviceClass.AQI, + icon="mdi:air-filter", name=ATTR_API_CAQI, native_unit_of_measurement="CAQI", ), diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 9ac10f20fc3..c95d2d895fd 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, @@ -37,7 +38,7 @@ async def test_sensor(hass, aioclient_mock): assert state.state == "7" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.AQI + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" entry = registry.async_get("sensor.home_caqi") assert entry From 97f5670fdcad7cd11fee4e13b23390747a738412 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 1 Oct 2022 17:22:23 +0300 Subject: [PATCH 054/183] Fix unifiprotect test failing CI (#79406) --- tests/components/unifiprotect/test_media_source.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 4b1e47d6b0c..8200a1323ab 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -711,19 +711,18 @@ async def test_browse_media_eventthumb( @freeze_time("2022-09-15 03:00:00-07:00") async def test_browse_media_day( - hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test browsing day selector level media.""" start = datetime.fromisoformat("2022-09-03 03:00:00-07:00") + end = datetime.fromisoformat("2022-09-15 03:00:00-07:00") ufp.api.bootstrap._recording_start = dt_util.as_utc(start) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) await init_entry(hass, ufp, [doorbell], regenerate_ids=False) - base_id = ( - f"test_id:browse:{doorbell.id}:all:range:{fixed_now.year}:{fixed_now.month}" - ) + base_id = f"test_id:browse:{doorbell.id}:all:range:{end.year}:{end.month}" source = await async_get_media_source(hass) media_item = MediaSourceItem(hass, DOMAIN, base_id, None) @@ -731,7 +730,7 @@ async def test_browse_media_day( assert ( browse.title - == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')}" + == f"UnifiProtect > {doorbell.name} > All Events > {end.strftime('%B %Y')}" ) assert browse.identifier == base_id assert len(browse.children) == 14 From 7e8905758bf180b400e37e2ea26a5a470d709785 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 1 Oct 2022 18:33:41 +0200 Subject: [PATCH 055/183] Fix low speed cover in Overkiz integration (#79416) Fix low speed cover --- .../components/overkiz/cover_entities/vertical_cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/cover_entities/vertical_cover.py b/homeassistant/components/overkiz/cover_entities/vertical_cover.py index f76f3849e83..90ac6428960 100644 --- a/homeassistant/components/overkiz/cover_entities/vertical_cover.py +++ b/homeassistant/components/overkiz/cover_entities/vertical_cover.py @@ -152,7 +152,7 @@ class LowSpeedCover(VerticalCover): ) -> None: """Initialize the device.""" super().__init__(device_url, coordinator) - self._attr_name = f"{self._attr_name} Low Speed" + self._attr_name = "Low speed" self._attr_unique_id = f"{self._attr_unique_id}_low_speed" async def async_set_cover_position(self, **kwargs: Any) -> None: From 046c3b4dd1859e206bdeef4bc60979358f2b944e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 2 Oct 2022 01:32:27 +0300 Subject: [PATCH 056/183] Bump aiowebostv to 0.2.1 (#79423) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 81c4d04901f..2547663be28 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.2.0"], + "requirements": ["aiowebostv==0.2.1"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 344b76a9283..2f9283df4f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -285,7 +285,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.2.0 +aiowebostv==0.2.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de1ab3b2738..74b5e4acb6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -260,7 +260,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.2.0 +aiowebostv==0.2.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 933b84050e6da724c1a811f790bba9386ab66d8b Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Sat, 1 Oct 2022 21:17:25 +0200 Subject: [PATCH 057/183] vicare: Don't create unsupportedd button entites (#79425) Button entities should only be offered when the datapoint exists on the API. --- homeassistant/components/vicare/__init__.py | 8 ++++++++ homeassistant/components/vicare/button.py | 20 ++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 20d237ee4e4..b177a4c524f 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -34,6 +34,14 @@ class ViCareRequiredKeysMixin: value_getter: Callable[[Device], bool] +@dataclass() +class ViCareRequiredKeysMixinWithSet: + """Mixin for required keys with setter.""" + + value_getter: Callable[[Device], bool] + value_setter: Callable[[Device], bool] + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from config entry.""" _LOGGER.debug("Setting up ViCare component") diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index b691c01796b..6f94c7102c9 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ViCareRequiredKeysMixin +from . import ViCareRequiredKeysMixinWithSet from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,9 @@ BUTTON_DHW_ACTIVATE_ONETIME_CHARGE = "activate_onetimecharge" @dataclass -class ViCareButtonEntityDescription(ButtonEntityDescription, ViCareRequiredKeysMixin): +class ViCareButtonEntityDescription( + ButtonEntityDescription, ViCareRequiredKeysMixinWithSet +): """Describes ViCare button sensor entity.""" @@ -37,7 +39,8 @@ BUTTON_DESCRIPTIONS: tuple[ViCareButtonEntityDescription, ...] = ( name="Activate one-time charge", icon="mdi:shower-head", entity_category=EntityCategory.CONFIG, - value_getter=lambda api: api.activateOneTimeCharge(), + value_getter=lambda api: api.getOneTimeCharge(), + value_setter=lambda api: api.activateOneTimeCharge(), ), ) @@ -54,6 +57,15 @@ async def async_setup_entry( entities = [] for description in BUTTON_DESCRIPTIONS: + try: + description.value_getter(api) + _LOGGER.debug("Found entity %s", description.name) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("Feature not supported %s", description.name) + continue + except AttributeError: + _LOGGER.debug("Attribute Error %s", name) + continue entity = ViCareButton( f"{name} {description.name}", api, @@ -83,7 +95,7 @@ class ViCareButton(ButtonEntity): """Handle the button press.""" try: with suppress(PyViCareNotSupportedFeatureError): - self.entity_description.value_getter(self._api) + self.entity_description.value_setter(self._api) except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") except ValueError: From a3833a408be219397a7ec06c4d7886bd0296b723 Mon Sep 17 00:00:00 2001 From: Matrix Date: Sun, 2 Oct 2022 06:56:36 +0800 Subject: [PATCH 058/183] Fix mqtt reconnect fail when token expired (#79428) * fix mqtt reconnect fail when token expired * suggest change --- homeassistant/components/yolink/__init__.py | 14 +++++++++++++- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 3257d64d265..06e6fd6472a 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -12,7 +12,7 @@ from yolink.model import BRDP from yolink.mqtt_client import MqttClient from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow @@ -110,11 +110,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_coordinators[device.device_id] = device_coordinator hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATORS] = device_coordinators await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + async def shutdown_subscription(event) -> None: + """Shutdown mqtt message subscription.""" + await yolink_mqtt_client.shutdown_home_subscription() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_subscription) + ) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + await hass.data[DOMAIN][entry.entry_id][ + ATTR_MQTT_CLIENT + ].shutdown_home_subscription() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 0db736938f7..665b17d9f22 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.9"], + "requirements": ["yolink-api==0.1.0"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index 2f9283df4f9..d277df9eaeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2577,7 +2577,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.9 +yolink-api==0.1.0 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74b5e4acb6d..ecd80751e53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1784,7 +1784,7 @@ yalexs==1.2.4 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.9 +yolink-api==0.1.0 # homeassistant.components.youless youless-api==0.16 From 2703bbc6300a99883a29e1c439d03b5aecc80ee2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 2 Oct 2022 02:43:15 +0200 Subject: [PATCH 059/183] Fix checking of upgrade API availability during setup of Synology DSM integration (#79435) --- homeassistant/components/synology_dsm/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 019108c3230..f57262e2a57 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -110,7 +110,7 @@ class SynoApi: # check if upgrade is available try: self.dsm.upgrade.update() - except SynologyDSMAPIErrorException as ex: + except SYNOLOGY_CONNECTION_EXCEPTIONS as ex: self._with_upgrade = False self.dsm.reset(SynoCoreUpgrade.API_KEY) LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex) From de90358f2a48d8e58c094dacb7635dcfb2cec7fa Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 2 Oct 2022 03:11:54 +0200 Subject: [PATCH 060/183] Fix Netatmo scope issue with HA cloud (#79437) Co-authored-by: Paulus Schoutsen --- homeassistant/components/netatmo/__init__.py | 14 ++++++++-- .../components/netatmo/config_flow.py | 9 ++++++- tests/components/netatmo/test_camera.py | 26 +++++++++---------- tests/components/netatmo/test_climate.py | 24 ++++++++--------- tests/components/netatmo/test_cover.py | 2 +- tests/components/netatmo/test_init.py | 2 +- tests/components/netatmo/test_light.py | 6 ++--- tests/components/netatmo/test_select.py | 2 +- tests/components/netatmo/test_sensor.py | 8 +++--- tests/components/netatmo/test_switch.py | 2 +- 10 files changed, 56 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 65b321d25aa..eb0e93c4b38 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -137,9 +137,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex raise ConfigEntryNotReady from ex - if sorted(session.token["scope"]) != sorted(NETATMO_SCOPES): + if entry.data["auth_implementation"] == cloud.DOMAIN: + required_scopes = { + scope + for scope in NETATMO_SCOPES + if scope not in ("access_doorbell", "read_doorbell") + } + else: + required_scopes = set(NETATMO_SCOPES) + + if not (set(session.token["scope"]) & required_scopes): _LOGGER.debug( - "Scope is invalid: %s != %s", session.token["scope"], NETATMO_SCOPES + "Session is missing scopes: %s", + required_scopes - set(session.token["scope"]), ) raise ConfigEntryAuthFailed("Token scope not valid, trigger renewal") diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 99fa195b118..acd8965d013 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -54,7 +54,14 @@ class NetatmoFlowHandler( @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" - return {"scope": " ".join(ALL_SCOPES)} + exclude = [] + if self.flow_impl.name == "Home Assistant Cloud": + exclude = ["access_doorbell", "read_doorbell"] + + scopes = [scope for scope in ALL_SCOPES if scope not in exclude] + scopes.sort() + + return {"scope": " ".join(scopes)} async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle a flow start.""" diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index ea39497ce58..027b0907d50 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -25,7 +25,7 @@ from tests.common import async_capture_events, async_fire_time_changed async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): """Test setup with webhook.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -132,7 +132,7 @@ IMAGE_BYTES_FROM_STREAM = b"test stream image bytes" async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_auth): """Test retrieval or local camera image.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -158,7 +158,7 @@ async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_aut async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth): """Test retrieval of remote camera image.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -182,7 +182,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) async def test_service_set_person_away(hass, config_entry, netatmo_auth): """Test service to set person as away.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -219,7 +219,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth): async def test_service_set_person_away_invalid_person(hass, config_entry, netatmo_auth): """Test service to set invalid person as away.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -247,7 +247,7 @@ async def test_service_set_persons_home_invalid_person( ): """Test service to set invalid persons as home.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -273,7 +273,7 @@ async def test_service_set_persons_home_invalid_person( async def test_service_set_persons_home(hass, config_entry, netatmo_auth): """Test service to set persons as home.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -297,7 +297,7 @@ async def test_service_set_persons_home(hass, config_entry, netatmo_auth): async def test_service_set_camera_light(hass, config_entry, netatmo_auth): """Test service to set the outdoor camera light mode.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -327,7 +327,7 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): async def test_service_set_camera_light_invalid_type(hass, config_entry, netatmo_auth): """Test service to set the indoor camera light mode.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -377,7 +377,7 @@ async def test_camera_reconnect_webhook(hass, config_entry): mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() mock_webhook.return_value = "https://example.com" - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -412,7 +412,7 @@ async def test_camera_reconnect_webhook(hass, config_entry): async def test_webhook_person_event(hass, config_entry, netatmo_auth): """Test that person events are handled.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -469,7 +469,7 @@ async def test_setup_component_no_devices(hass, config_entry): mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert fake_post_hits == 9 @@ -508,7 +508,7 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock): mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() camera_entity_indoor = "camera.hall" diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index cc23dc887bd..d37bab929e1 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -27,7 +27,7 @@ from .common import selected_platforms, simulate_webhook async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_auth): """Test service and webhook event handling with thermostats.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -204,7 +204,7 @@ async def test_service_preset_mode_frost_guard_thermostat( ): """Test service with frost guard preset for thermostats.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -277,7 +277,7 @@ async def test_service_preset_mode_frost_guard_thermostat( async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth): """Test service with preset modes for thermostats.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -356,7 +356,7 @@ async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth) async def test_webhook_event_handling_no_data(hass, config_entry, netatmo_auth): """Test service and webhook event handling with erroneous data.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -405,7 +405,7 @@ async def test_webhook_event_handling_no_data(hass, config_entry, netatmo_auth): async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_auth): """Test service for selecting Netatmo schedule with thermostats.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -458,7 +458,7 @@ async def test_service_preset_mode_already_boost_valves( ): """Test service with boost preset for valves when already in boost mode.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -536,7 +536,7 @@ async def test_service_preset_mode_already_boost_valves( async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth): """Test service with boost preset for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -586,7 +586,7 @@ async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth async def test_service_preset_mode_invalid(hass, config_entry, caplog, netatmo_auth): """Test service with invalid preset.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -604,7 +604,7 @@ async def test_service_preset_mode_invalid(hass, config_entry, caplog, netatmo_a async def test_valves_service_turn_off(hass, config_entry, netatmo_auth): """Test service turn off for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -654,7 +654,7 @@ async def test_valves_service_turn_off(hass, config_entry, netatmo_auth): async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -699,7 +699,7 @@ async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -737,7 +737,7 @@ async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): async def test_webhook_set_point(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_cover.py b/tests/components/netatmo/test_cover.py index c0f34f25b24..6a5709ebf8f 100644 --- a/tests/components/netatmo/test_cover.py +++ b/tests/components/netatmo/test_cover.py @@ -17,7 +17,7 @@ from .common import selected_platforms async def test_cover_setup_and_services(hass, config_entry, netatmo_auth): """Test setup and services.""" with selected_platforms(["cover"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 373d7e19765..187a89afeb6 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -121,7 +121,7 @@ async def test_setup_component_with_config(hass, config_entry): async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): """Test setup and teardown of the netatmo component with webhook registration.""" with selected_platforms(["camera", "climate", "light", "sensor"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index ced24c738e3..b1a5270745c 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -17,7 +17,7 @@ from tests.test_util.aiohttp import AiohttpClientMockResponse async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth): """Test camera ligiht setup and services.""" with selected_platforms(["light"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -108,7 +108,7 @@ async def test_setup_component_no_devices(hass, config_entry): mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # Fake webhook activation @@ -126,7 +126,7 @@ async def test_setup_component_no_devices(hass, config_entry): async def test_light_setup_and_services(hass, config_entry, netatmo_auth): """Test setup and services.""" with selected_platforms(["light"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index ea8e88ce8de..8053b8bdac7 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -14,7 +14,7 @@ from .common import selected_platforms, simulate_webhook async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_auth): """Test service for selecting Netatmo schedule with thermostats.""" with selected_platforms(["climate", "select"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 99e76389c13..d3ea8fb8167 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -12,7 +12,7 @@ from .common import TEST_TIME, selected_platforms async def test_weather_sensor(hass, config_entry, netatmo_auth): """Test weather sensor setup.""" with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -27,7 +27,7 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth): async def test_public_weather_sensor(hass, config_entry, netatmo_auth): """Test public weather sensor setup.""" with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -182,7 +182,7 @@ async def test_weather_sensor_enabling( suggested_object_id=name, disabled_by=None, ) - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -195,7 +195,7 @@ async def test_climate_battery_sensor(hass, config_entry, netatmo_auth): with patch("time.time", return_value=TEST_TIME), selected_platforms( ["sensor", "climate"] ): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_switch.py b/tests/components/netatmo/test_switch.py index dc11ac22746..c6f9c9c514e 100644 --- a/tests/components/netatmo/test_switch.py +++ b/tests/components/netatmo/test_switch.py @@ -14,7 +14,7 @@ from .common import selected_platforms async def test_switch_setup_and_services(hass, config_entry, netatmo_auth): """Test setup and services.""" with selected_platforms(["switch"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 71e320fc96071cbc403df43ce1f85c3d28232c0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 14:45:01 -1000 Subject: [PATCH 061/183] Bump dbus-fast to 1.18.0 (#79440) Changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.17.0...v1.18.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9a2f1f0e901..e4563038569 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.5.2", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.17.0" + "dbus-fast==1.18.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 38b6ee1e52b..4dfd94862a2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.17.0 +dbus-fast==1.18.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index d277df9eaeb..d819a1f7a73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.17.0 +dbus-fast==1.18.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecd80751e53..c6c955fccb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.17.0 +dbus-fast==1.18.0 # homeassistant.components.debugpy debugpy==1.6.3 From 2fed773b93ab52ade1fe98a5308d8dfc21b93c03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 15:27:44 -1000 Subject: [PATCH 062/183] Bump bluetooth-adapters to 0.5.3 (#79442) --- 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 e4563038569..9c989bfe9fa 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -8,7 +8,7 @@ "requirements": [ "bleak==0.18.1", "bleak-retry-connector==2.1.3", - "bluetooth-adapters==0.5.2", + "bluetooth-adapters==0.5.3", "bluetooth-auto-recovery==0.3.3", "dbus-fast==1.18.0" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4dfd94862a2..258776dde30 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.1.3 bleak==0.18.1 -bluetooth-adapters==0.5.2 +bluetooth-adapters==0.5.3 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d819a1f7a73..ce6b0657c3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -435,7 +435,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.2 +bluetooth-adapters==0.5.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6c955fccb2..e261402f212 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,7 +349,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.2 +bluetooth-adapters==0.5.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 From 6c7060e0e2dfb859adf5e01953176d3164f1d985 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 14:48:09 -1000 Subject: [PATCH 063/183] Bump ibeacon-ble to 0.7.3 (#79443) --- homeassistant/components/ibeacon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json index 7b4110a7fe4..a2b55a69403 100644 --- a/homeassistant/components/ibeacon/manifest.json +++ b/homeassistant/components/ibeacon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ibeacon", "dependencies": ["bluetooth"], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], - "requirements": ["ibeacon_ble==0.7.2"], + "requirements": ["ibeacon_ble==0.7.3"], "codeowners": ["@bdraco"], "iot_class": "local_push", "loggers": ["bleak"], diff --git a/requirements_all.txt b/requirements_all.txt index ce6b0657c3a..314ee14c434 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -898,7 +898,7 @@ iammeter==0.1.7 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.2 +ibeacon_ble==0.7.3 # homeassistant.components.watson_tts ibm-watson==5.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e261402f212..71c241214e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -669,7 +669,7 @@ hyperion-py==0.7.5 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.2 +ibeacon_ble==0.7.3 # homeassistant.components.ping icmplib==3.0 From 590d47aad0340266ddcad0a6aad12e9a6bf0ed1a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Oct 2022 21:28:46 -0400 Subject: [PATCH 064/183] Bumped version to 2022.10.0b3 --- 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 8b5ad8f9666..1415a720c83 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 = 10 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __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 294e90fecf5..464cefbda94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0b2" +version = "2022.10.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d41d09aa7ef8d2e65b6b3484d6e833e5df366cf2 Mon Sep 17 00:00:00 2001 From: Nyro Date: Mon, 3 Oct 2022 03:22:20 +0200 Subject: [PATCH 065/183] Fix overkiz entity name (#79229) --- homeassistant/components/overkiz/entity.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 4cb5ad1ede7..c17f30393fc 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -34,8 +34,17 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): self._attr_available = self.device.available self._attr_unique_id = self.device.device_url + if self.is_sub_device: + # In case of sub entity, use the provided label as name + self._attr_name = self.device.label + self._attr_device_info = self.generate_device_info() + @property + def is_sub_device(self) -> bool: + """Return True if device is a sub device.""" + return "#" in self.device_url and not self.device_url.endswith("#1") + @property def device(self) -> Device: """Return Overkiz device linked to this entity.""" @@ -46,7 +55,7 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): # Some devices, such as the Smart Thermostat have several devices in one physical device, # with same device url, terminated by '#' and a number. # In this case, we use the base device url as the device identifier. - if "#" in self.device_url and not self.device_url.endswith("#1"): + if self.is_sub_device: # Only return the url of the base device, to inherit device name and model from parent device. return { "identifiers": {(DOMAIN, self.executor.base_device_url)}, @@ -103,6 +112,10 @@ class OverkizDescriptiveEntity(OverkizEntity): self.entity_description = description self._attr_unique_id = f"{super().unique_id}-{self.entity_description.key}" + if self.is_sub_device: + # In case of sub device, use the provided label and append the name of the type of entity + self._attr_name = f"{self.device.label} {description.name}" + # Used by state translations for sensor and select entities @unique From e410e298c4b130279fd9d5ec36a0185a8fb302d7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 1 Oct 2022 18:55:00 +0200 Subject: [PATCH 066/183] Remove state_unit_of_measurement from metadata DB table (#79370) * Remove state_unit_of_measurement from metadata DB table * Adjust test --- homeassistant/components/demo/__init__.py | 5 - .../components/recorder/db_schema.py | 1 - .../components/recorder/migration.py | 21 +- homeassistant/components/recorder/models.py | 1 - .../components/recorder/statistics.py | 36 +- .../components/recorder/websocket_api.py | 1 - homeassistant/components/sensor/recorder.py | 3 - homeassistant/components/tibber/sensor.py | 1 - tests/components/demo/test_init.py | 3 - tests/components/energy/test_websocket_api.py | 9 - tests/components/recorder/db_schema_29.py | 616 ------------------ tests/components/recorder/test_migrate.py | 115 +--- tests/components/recorder/test_statistics.py | 63 +- .../components/recorder/test_websocket_api.py | 21 - tests/components/sensor/test_recorder.py | 37 -- 15 files changed, 25 insertions(+), 908 deletions(-) delete mode 100644 tests/components/recorder/db_schema_29.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 4d0ef03c564..7ed989903e5 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -295,7 +295,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata: StatisticMetaData = { "source": DOMAIN, "name": "Outdoor temperature", - "state_unit_of_measurement": TEMP_CELSIUS, "statistic_id": f"{DOMAIN}:temperature_outdoor", "unit_of_measurement": TEMP_CELSIUS, "has_mean": True, @@ -309,7 +308,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Energy consumption 1", - "state_unit_of_measurement": ENERGY_KILO_WATT_HOUR, "statistic_id": f"{DOMAIN}:energy_consumption_kwh", "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "has_mean": False, @@ -322,7 +320,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Energy consumption 2", - "state_unit_of_measurement": ENERGY_MEGA_WATT_HOUR, "statistic_id": f"{DOMAIN}:energy_consumption_mwh", "unit_of_measurement": ENERGY_MEGA_WATT_HOUR, "has_mean": False, @@ -337,7 +334,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Gas consumption 1", - "state_unit_of_measurement": VOLUME_CUBIC_METERS, "statistic_id": f"{DOMAIN}:gas_consumption_m3", "unit_of_measurement": VOLUME_CUBIC_METERS, "has_mean": False, @@ -352,7 +348,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Gas consumption 2", - "state_unit_of_measurement": VOLUME_CUBIC_FEET, "statistic_id": f"{DOMAIN}:gas_consumption_ft3", "unit_of_measurement": VOLUME_CUBIC_FEET, "has_mean": False, diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 363604d525b..d76f89068d0 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -494,7 +494,6 @@ class StatisticsMeta(Base): # type: ignore[misc,valid-type] id = Column(Integer, Identity(), primary_key=True) statistic_id = Column(String(255), index=True, unique=True) source = Column(String(32)) - state_unit_of_measurement = Column(String(255)) unit_of_measurement = Column(String(255)) has_mean = Column(Boolean) has_sum = Column(Boolean) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index e2169727382..f82ec7ba1eb 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -748,24 +748,9 @@ def _apply_update( # noqa: C901 session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" ) elif new_version == 30: - _add_columns( - session_maker, - "statistics_meta", - ["state_unit_of_measurement VARCHAR(255)"], - ) - # When querying the database, be careful to only explicitly query for columns - # which were present in schema version 30. If querying the table, SQLAlchemy - # will refer to future columns. - with session_scope(session=session_maker()) as session: - for statistics_meta in session.query( - StatisticsMeta.id, StatisticsMeta.unit_of_measurement - ): - session.query(StatisticsMeta).filter_by(id=statistics_meta.id).update( - { - StatisticsMeta.state_unit_of_measurement: statistics_meta.unit_of_measurement, - }, - synchronize_session=False, - ) + # This added a column to the statistics_meta table, removed again before + # release of HA Core 2022.10.0 + pass else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 4f6d8a990a6..cfc797cf7ea 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -64,7 +64,6 @@ class StatisticMetaData(TypedDict): has_sum: bool name: str | None source: str - state_unit_of_measurement: str | None statistic_id: str unit_of_measurement: str | None diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index a0ff73b10fd..ef066b82060 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -24,6 +24,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Subquery import voluptuous as vol +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry @@ -116,7 +117,6 @@ QUERY_STATISTIC_META = [ StatisticsMeta.id, StatisticsMeta.statistic_id, StatisticsMeta.source, - StatisticsMeta.state_unit_of_measurement, StatisticsMeta.unit_of_measurement, StatisticsMeta.has_mean, StatisticsMeta.has_sum, @@ -342,8 +342,6 @@ def _update_or_add_metadata( old_metadata["has_mean"] != new_metadata["has_mean"] or old_metadata["has_sum"] != new_metadata["has_sum"] or old_metadata["name"] != new_metadata["name"] - or old_metadata["state_unit_of_measurement"] - != new_metadata["state_unit_of_measurement"] or old_metadata["unit_of_measurement"] != new_metadata["unit_of_measurement"] ): session.query(StatisticsMeta).filter_by(statistic_id=statistic_id).update( @@ -351,9 +349,6 @@ def _update_or_add_metadata( StatisticsMeta.has_mean: new_metadata["has_mean"], StatisticsMeta.has_sum: new_metadata["has_sum"], StatisticsMeta.name: new_metadata["name"], - StatisticsMeta.state_unit_of_measurement: new_metadata[ - "state_unit_of_measurement" - ], StatisticsMeta.unit_of_measurement: new_metadata["unit_of_measurement"], }, synchronize_session=False, @@ -820,7 +815,6 @@ def get_metadata_with_session( "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "state_unit_of_measurement": meta["state_unit_of_measurement"], "statistic_id": meta["statistic_id"], "unit_of_measurement": meta["unit_of_measurement"], }, @@ -899,7 +893,6 @@ def list_statistic_ids( result = { meta["statistic_id"]: { - "state_unit_of_measurement": meta["state_unit_of_measurement"], "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], @@ -926,7 +919,6 @@ def list_statistic_ids( "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "state_unit_of_measurement": meta["state_unit_of_measurement"], "unit_class": _get_unit_class(meta["unit_of_measurement"]), "unit_of_measurement": meta["unit_of_measurement"], } @@ -939,7 +931,6 @@ def list_statistic_ids( "has_sum": info["has_sum"], "name": info.get("name"), "source": info["source"], - "state_unit_of_measurement": info["state_unit_of_measurement"], "statistics_unit_of_measurement": info["unit_of_measurement"], "unit_class": info["unit_class"], } @@ -1386,9 +1377,10 @@ def _sorted_statistics_to_dict( # Append all statistic entries, and optionally do unit conversion for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore[no-any-return] - unit = metadata[meta_id]["unit_of_measurement"] - state_unit = metadata[meta_id]["state_unit_of_measurement"] + state_unit = unit = metadata[meta_id]["unit_of_measurement"] statistic_id = metadata[meta_id]["statistic_id"] + if state := hass.states.get(statistic_id): + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit is not None and convert_units: convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) else: @@ -1470,18 +1462,6 @@ def _async_import_statistics( get_instance(hass).async_import_statistics(metadata, statistics) -def _validate_units(statistics_unit: str | None, state_unit: str | None) -> None: - """Raise if the statistics unit and state unit are not compatible.""" - if statistics_unit == state_unit: - return - if ( - unit_converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistics_unit) - ) is None: - raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}") - if state_unit not in unit_converter.VALID_UNITS: - raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}") - - @callback def async_import_statistics( hass: HomeAssistant, @@ -1499,10 +1479,6 @@ def async_import_statistics( if not metadata["source"] or metadata["source"] != DOMAIN: raise HomeAssistantError("Invalid source") - _validate_units( - metadata["unit_of_measurement"], metadata["state_unit_of_measurement"] - ) - _async_import_statistics(hass, metadata, statistics) @@ -1525,10 +1501,6 @@ def async_add_external_statistics( if not metadata["source"] or metadata["source"] != domain: raise HomeAssistantError("Invalid source") - _validate_units( - metadata["unit_of_measurement"], metadata["state_unit_of_measurement"] - ) - _async_import_statistics(hass, metadata, statistics) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 02b7519486d..c583577ec8f 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -376,7 +376,6 @@ def ws_import_statistics( """Import statistics.""" metadata = msg["metadata"] stats = msg["stats"] - metadata["state_unit_of_measurement"] = metadata["unit_of_measurement"] if valid_entity_id(metadata["statistic_id"]): async_import_statistics(hass, metadata, stats) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 5241b123185..4380efbd2c3 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -480,7 +480,6 @@ def _compile_statistics( # noqa: C901 "has_sum": "sum" in wanted_statistics[entity_id], "name": None, "source": RECORDER_DOMAIN, - "state_unit_of_measurement": state_unit, "statistic_id": entity_id, "unit_of_measurement": normalized_unit, } @@ -621,7 +620,6 @@ def list_statistic_ids( "has_sum": "sum" in provided_statistics, "name": None, "source": RECORDER_DOMAIN, - "state_unit_of_measurement": state_unit, "statistic_id": state.entity_id, "unit_of_measurement": state_unit, } @@ -637,7 +635,6 @@ def list_statistic_ids( "has_sum": "sum" in provided_statistics, "name": None, "source": RECORDER_DOMAIN, - "state_unit_of_measurement": state_unit, "statistic_id": state.entity_id, "unit_of_measurement": statistics_unit, } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 93fdba107ed..ca0c253590f 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -642,7 +642,6 @@ class TibberDataCoordinator(DataUpdateCoordinator): has_sum=True, name=f"{home.name} {sensor_type}", source=TIBBER_DOMAIN, - state_unit_of_measurement=unit, statistic_id=statistic_id, unit_of_measurement=unit, ) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 8c3adeb1c98..f7a89fe63c1 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -67,7 +67,6 @@ async def test_demo_statistics(hass, recorder_mock): "has_sum": False, "name": "Outdoor temperature", "source": "demo", - "state_unit_of_measurement": "°C", "statistic_id": "demo:temperature_outdoor", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", @@ -77,7 +76,6 @@ async def test_demo_statistics(hass, recorder_mock): "has_sum": True, "name": "Energy consumption 1", "source": "demo", - "state_unit_of_measurement": "kWh", "statistic_id": "demo:energy_consumption_kwh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", @@ -96,7 +94,6 @@ async def test_demo_statistics_growth(hass, recorder_mock): metadata = { "source": DOMAIN, "name": "Energy consumption 1", - "state_unit_of_measurement": "m³", "statistic_id": statistic_id, "unit_of_measurement": "m³", "has_mean": False, diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index cbe0e47124f..ab785291f91 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -336,7 +336,6 @@ async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_m "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -371,7 +370,6 @@ async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_m "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -499,7 +497,6 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_moc "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -534,7 +531,6 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_moc "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -660,7 +656,6 @@ async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_ "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -695,7 +690,6 @@ async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_ "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -812,7 +806,6 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -847,7 +840,6 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -878,7 +870,6 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "has_sum": False, "name": "Fossil percentage", "source": "test", - "state_unit_of_measurement": "%", "statistic_id": "test:fossil_percentage", "unit_of_measurement": "%", } diff --git a/tests/components/recorder/db_schema_29.py b/tests/components/recorder/db_schema_29.py deleted file mode 100644 index 54aa4b2b13c..00000000000 --- a/tests/components/recorder/db_schema_29.py +++ /dev/null @@ -1,616 +0,0 @@ -"""Models for SQLAlchemy. - -This file contains the model definitions for schema version 28. -It is used to test the schema migration logic. -""" -from __future__ import annotations - -from collections.abc import Callable -from datetime import datetime, timedelta -import logging -from typing import Any, TypeVar, cast - -import ciso8601 -from fnvhash import fnv1a_32 -from sqlalchemy import ( - JSON, - BigInteger, - Boolean, - Column, - DateTime, - Float, - ForeignKey, - Identity, - Index, - Integer, - SmallInteger, - String, - Text, - distinct, - type_coerce, -) -from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite -from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.orm import aliased, declarative_base, relationship -from sqlalchemy.orm.session import Session - -from homeassistant.components.recorder.const import ALL_DOMAIN_EXCLUDE_ATTRS -from homeassistant.components.recorder.models import ( - StatisticData, - StatisticMetaData, - process_timestamp, -) -from homeassistant.const import ( - MAX_LENGTH_EVENT_CONTEXT_ID, - MAX_LENGTH_EVENT_EVENT_TYPE, - MAX_LENGTH_EVENT_ORIGIN, - MAX_LENGTH_STATE_ENTITY_ID, - MAX_LENGTH_STATE_STATE, -) -from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id -from homeassistant.helpers.json import ( - JSON_DECODE_EXCEPTIONS, - JSON_DUMP, - json_bytes, - json_loads, -) -import homeassistant.util.dt as dt_util - -# SQLAlchemy Schema -# pylint: disable=invalid-name -Base = declarative_base() - -SCHEMA_VERSION = 29 - -_StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") - -_LOGGER = logging.getLogger(__name__) - -TABLE_EVENTS = "events" -TABLE_EVENT_DATA = "event_data" -TABLE_STATES = "states" -TABLE_STATE_ATTRIBUTES = "state_attributes" -TABLE_RECORDER_RUNS = "recorder_runs" -TABLE_SCHEMA_CHANGES = "schema_changes" -TABLE_STATISTICS = "statistics" -TABLE_STATISTICS_META = "statistics_meta" -TABLE_STATISTICS_RUNS = "statistics_runs" -TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" - -ALL_TABLES = [ - TABLE_STATES, - TABLE_STATE_ATTRIBUTES, - TABLE_EVENTS, - TABLE_EVENT_DATA, - TABLE_RECORDER_RUNS, - TABLE_SCHEMA_CHANGES, - TABLE_STATISTICS, - TABLE_STATISTICS_META, - TABLE_STATISTICS_RUNS, - TABLE_STATISTICS_SHORT_TERM, -] - -TABLES_TO_CHECK = [ - TABLE_STATES, - TABLE_EVENTS, - TABLE_RECORDER_RUNS, - TABLE_SCHEMA_CHANGES, -] - -LAST_UPDATED_INDEX = "ix_states_last_updated" -ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" -EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" -STATES_CONTEXT_ID_INDEX = "ix_states_context_id" - - -class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] - """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" - - def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] - """Offload the datetime parsing to ciso8601.""" - return lambda value: None if value is None else ciso8601.parse_datetime(value) - - -JSON_VARIENT_CAST = Text().with_variant( - postgresql.JSON(none_as_null=True), "postgresql" -) -JSONB_VARIENT_CAST = Text().with_variant( - postgresql.JSONB(none_as_null=True), "postgresql" -) -DATETIME_TYPE = ( - DateTime(timezone=True) - .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql") - .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") -) -DOUBLE_TYPE = ( - Float() - .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") - .with_variant(oracle.DOUBLE_PRECISION(), "oracle") - .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") -) - - -class JSONLiteral(JSON): # type: ignore[misc] - """Teach SA how to literalize json.""" - - def literal_processor(self, dialect: str) -> Callable[[Any], str]: - """Processor to convert a value to JSON.""" - - def process(value: Any) -> str: - """Dump json.""" - return JSON_DUMP(value) - - return process - - -EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] -EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} - - -class Events(Base): # type: ignore[misc,valid-type] - """Event history data.""" - - __table_args__ = ( - # Used for fetching events at a specific time - # see logbook - Index("ix_events_event_type_time_fired", "event_type", "time_fired"), - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_EVENTS - event_id = Column(Integer, Identity(), primary_key=True) - event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) - event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used for new rows - origin_idx = Column(SmallInteger) - time_fired = Column(DATETIME_TYPE, index=True) - context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - data_id = Column(Integer, ForeignKey("event_data.data_id"), index=True) - event_data_rel = relationship("EventData") - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> Events: - """Create an event database object from a native event.""" - return Events( - event_type=event.event_type, - event_data=None, - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - time_fired=event.time_fired, - context_id=event.context.id, - context_user_id=event.context.user_id, - context_parent_id=event.context.parent_id, - ) - - def to_native(self, validate_entity_id: bool = True) -> Event | None: - """Convert to a native HA Event.""" - context = Context( - id=self.context_id, - user_id=self.context_user_id, - parent_id=self.context_parent_id, - ) - try: - return Event( - self.event_type, - json_loads(self.event_data) if self.event_data else {}, - EventOrigin(self.origin) - if self.origin - else EVENT_ORIGIN_ORDER[self.origin_idx], - process_timestamp(self.time_fired), - context=context, - ) - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting to event: %s", self) - return None - - -class EventData(Base): # type: ignore[misc,valid-type] - """Event data history.""" - - __table_args__ = ( - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_EVENT_DATA - data_id = Column(Integer, Identity(), primary_key=True) - hash = Column(BigInteger, index=True) - # Note that this is not named attributes to avoid confusion with the states table - shared_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> EventData: - """Create object from an event.""" - shared_data = json_bytes(event.data) - return EventData( - shared_data=shared_data.decode("utf-8"), - hash=EventData.hash_shared_data_bytes(shared_data), - ) - - @staticmethod - def shared_data_bytes_from_event(event: Event) -> bytes: - """Create shared_data from an event.""" - return json_bytes(event.data) - - @staticmethod - def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: - """Return the hash of json encoded shared data.""" - return cast(int, fnv1a_32(shared_data_bytes)) - - def to_native(self) -> dict[str, Any]: - """Convert to an HA state object.""" - try: - return cast(dict[str, Any], json_loads(self.shared_data)) - except JSON_DECODE_EXCEPTIONS: - _LOGGER.exception("Error converting row to event data: %s", self) - return {} - - -class States(Base): # type: ignore[misc,valid-type] - """State change history.""" - - __table_args__ = ( - # Used for fetching the state of entities at a specific time - # (get_states in history.py) - Index(ENTITY_ID_LAST_UPDATED_INDEX, "entity_id", "last_updated"), - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_STATES - state_id = Column(Integer, Identity(), primary_key=True) - entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) - state = Column(String(MAX_LENGTH_STATE_STATE)) - attributes = Column( - Text().with_variant(mysql.LONGTEXT, "mysql") - ) # no longer used for new rows - event_id = Column( # no longer used for new rows - Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True - ) - last_changed = Column(DATETIME_TYPE) - last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) - old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) - attributes_id = Column( - Integer, ForeignKey("state_attributes.attributes_id"), index=True - ) - context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - origin_idx = Column(SmallInteger) # 0 is local, 1 is remote - old_state = relationship("States", remote_side=[state_id]) - state_attributes = relationship("StateAttributes") - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> States: - """Create object from a state_changed event.""" - entity_id = event.data["entity_id"] - state: State | None = event.data.get("new_state") - dbstate = States( - entity_id=entity_id, - attributes=None, - context_id=event.context.id, - context_user_id=event.context.user_id, - context_parent_id=event.context.parent_id, - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - ) - - # None state means the state was removed from the state machine - if state is None: - dbstate.state = "" - dbstate.last_updated = event.time_fired - dbstate.last_changed = None - return dbstate - - dbstate.state = state.state - dbstate.last_updated = state.last_updated - if state.last_updated == state.last_changed: - dbstate.last_changed = None - else: - dbstate.last_changed = state.last_changed - - return dbstate - - def to_native(self, validate_entity_id: bool = True) -> State | None: - """Convert to an HA state object.""" - context = Context( - id=self.context_id, - user_id=self.context_user_id, - parent_id=self.context_parent_id, - ) - try: - attrs = json_loads(self.attributes) if self.attributes else {} - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting row to state: %s", self) - return None - if self.last_changed is None or self.last_changed == self.last_updated: - last_changed = last_updated = process_timestamp(self.last_updated) - else: - last_updated = process_timestamp(self.last_updated) - last_changed = process_timestamp(self.last_changed) - return State( - self.entity_id, - self.state, - # Join the state_attributes table on attributes_id to get the attributes - # for newer states - attrs, - last_changed, - last_updated, - context=context, - validate_entity_id=validate_entity_id, - ) - - -class StateAttributes(Base): # type: ignore[misc,valid-type] - """State attribute change history.""" - - __table_args__ = ( - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_STATE_ATTRIBUTES - attributes_id = Column(Integer, Identity(), primary_key=True) - hash = Column(BigInteger, index=True) - # Note that this is not named attributes to avoid confusion with the states table - shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> StateAttributes: - """Create object from a state_changed event.""" - state: State | None = event.data.get("new_state") - # None state means the state was removed from the state machine - attr_bytes = b"{}" if state is None else json_bytes(state.attributes) - dbstate = StateAttributes(shared_attrs=attr_bytes.decode("utf-8")) - dbstate.hash = StateAttributes.hash_shared_attrs_bytes(attr_bytes) - return dbstate - - @staticmethod - def shared_attrs_bytes_from_event( - event: Event, exclude_attrs_by_domain: dict[str, set[str]] - ) -> bytes: - """Create shared_attrs from a state_changed event.""" - state: State | None = event.data.get("new_state") - # None state means the state was removed from the state machine - if state is None: - return b"{}" - domain = split_entity_id(state.entity_id)[0] - exclude_attrs = ( - exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS - ) - return json_bytes( - {k: v for k, v in state.attributes.items() if k not in exclude_attrs} - ) - - @staticmethod - def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: - """Return the hash of json encoded shared attributes.""" - return cast(int, fnv1a_32(shared_attrs_bytes)) - - def to_native(self) -> dict[str, Any]: - """Convert to an HA state object.""" - try: - return cast(dict[str, Any], json_loads(self.shared_attrs)) - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting row to state attributes: %s", self) - return {} - - -class StatisticsBase: - """Statistics base class.""" - - id = Column(Integer, Identity(), primary_key=True) - created = Column(DATETIME_TYPE, default=dt_util.utcnow) - - @declared_attr # type: ignore[misc] - def metadata_id(self) -> Column: - """Define the metadata_id column for sub classes.""" - return Column( - Integer, - ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), - index=True, - ) - - start = Column(DATETIME_TYPE, index=True) - mean = Column(DOUBLE_TYPE) - min = Column(DOUBLE_TYPE) - max = Column(DOUBLE_TYPE) - last_reset = Column(DATETIME_TYPE) - state = Column(DOUBLE_TYPE) - sum = Column(DOUBLE_TYPE) - - @classmethod - def from_stats( - cls: type[_StatisticsBaseSelfT], metadata_id: int, stats: StatisticData - ) -> _StatisticsBaseSelfT: - """Create object from a statistics.""" - return cls( # type: ignore[call-arg,misc] - metadata_id=metadata_id, - **stats, - ) - - -class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] - """Long term statistics.""" - - duration = timedelta(hours=1) - - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), - ) - __tablename__ = TABLE_STATISTICS - - -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] - """Short term statistics.""" - - duration = timedelta(minutes=5) - - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index( - "ix_statistics_short_term_statistic_id_start", - "metadata_id", - "start", - unique=True, - ), - ) - __tablename__ = TABLE_STATISTICS_SHORT_TERM - - -class StatisticsMeta(Base): # type: ignore[misc,valid-type] - """Statistics meta data.""" - - __table_args__ = ( - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_STATISTICS_META - id = Column(Integer, Identity(), primary_key=True) - statistic_id = Column(String(255), index=True, unique=True) - source = Column(String(32)) - unit_of_measurement = Column(String(255)) - has_mean = Column(Boolean) - has_sum = Column(Boolean) - name = Column(String(255)) - - @staticmethod - def from_meta(meta: StatisticMetaData) -> StatisticsMeta: - """Create object from meta data.""" - return StatisticsMeta(**meta) - - -class RecorderRuns(Base): # type: ignore[misc,valid-type] - """Representation of recorder run.""" - - __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) - __tablename__ = TABLE_RECORDER_RUNS - run_id = Column(Integer, Identity(), primary_key=True) - start = Column(DateTime(timezone=True), default=dt_util.utcnow) - end = Column(DateTime(timezone=True)) - closed_incorrect = Column(Boolean, default=False) - created = Column(DateTime(timezone=True), default=dt_util.utcnow) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - end = ( - f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None - ) - return ( - f"" - ) - - def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: - """Return the entity ids that existed in this run. - - Specify point_in_time if you want to know which existed at that point - in time inside the run. - """ - session = Session.object_session(self) - - assert session is not None, "RecorderRuns need to be persisted" - - query = session.query(distinct(States.entity_id)).filter( - States.last_updated >= self.start - ) - - if point_in_time is not None: - query = query.filter(States.last_updated < point_in_time) - elif self.end is not None: - query = query.filter(States.last_updated < self.end) - - return [row[0] for row in query] - - def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: - """Return self, native format is this model.""" - return self - - -class SchemaChanges(Base): # type: ignore[misc,valid-type] - """Representation of schema version changes.""" - - __tablename__ = TABLE_SCHEMA_CHANGES - change_id = Column(Integer, Identity(), primary_key=True) - schema_version = Column(Integer) - changed = Column(DateTime(timezone=True), default=dt_util.utcnow) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - -class StatisticsRuns(Base): # type: ignore[misc,valid-type] - """Representation of statistics run.""" - - __tablename__ = TABLE_STATISTICS_RUNS - run_id = Column(Integer, Identity(), primary_key=True) - start = Column(DateTime(timezone=True), index=True) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - -EVENT_DATA_JSON = type_coerce( - EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) -OLD_FORMAT_EVENT_DATA_JSON = type_coerce( - Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) - -SHARED_ATTRS_JSON = type_coerce( - StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) -OLD_FORMAT_ATTRS_JSON = type_coerce( - States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) - -ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] -OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] -DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] -OLD_STATE = aliased(States, name="old_state") diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index cbba4dab26b..9e0609de5b6 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -21,21 +21,18 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import db_schema, migration -from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, RecorderRuns, States, ) -from homeassistant.components.recorder.statistics import get_start_time from homeassistant.components.recorder.util import session_scope from homeassistant.helpers import recorder as recorder_helper -from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from .common import async_wait_recording_done, create_engine_test, wait_recording_done +from .common import async_wait_recording_done, create_engine_test -from tests.common import async_fire_time_changed, get_test_home_assistant +from tests.common import async_fire_time_changed ORIG_TZ = dt_util.DEFAULT_TIME_ZONE @@ -363,114 +360,6 @@ async def test_schema_migrate(hass, start_version, live): assert recorder.util.async_migration_in_progress(hass) is not True -def test_set_state_unit(caplog, tmpdir): - """Test state unit column is initialized.""" - - def _create_engine_29(*args, **kwargs): - """Test version of create_engine that initializes with old schema. - - This simulates an existing db with the old schema. - """ - module = "tests.components.recorder.db_schema_29" - importlib.import_module(module) - old_db_schema = sys.modules[module] - engine = create_engine(*args, **kwargs) - old_db_schema.Base.metadata.create_all(engine) - with Session(engine) as session: - session.add(recorder.db_schema.StatisticsRuns(start=get_start_time())) - session.add( - recorder.db_schema.SchemaChanges( - schema_version=old_db_schema.SCHEMA_VERSION - ) - ) - session.commit() - return engine - - test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - - module = "tests.components.recorder.db_schema_29" - importlib.import_module(module) - old_db_schema = sys.modules[module] - - external_energy_metadata_1 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_1", - "unit_of_measurement": "kWh", - } - external_co2_metadata = { - "has_mean": True, - "has_sum": False, - "name": "Fossil percentage", - "source": "test", - "statistic_id": "test:fossil_percentage", - "unit_of_measurement": "%", - } - - # Create some statistics_meta with schema version 29 - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch( - "homeassistant.components.recorder.core.create_engine", new=_create_engine_29 - ): - hass = get_test_home_assistant() - recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - session.add( - recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_1) - ) - session.add( - recorder.db_schema.StatisticsMeta.from_meta(external_co2_metadata) - ) - - with session_scope(hass=hass) as session: - tmp = session.query(recorder.db_schema.StatisticsMeta).all() - assert len(tmp) == 2 - assert tmp[0].id == 1 - assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" - assert tmp[0].unit_of_measurement == "kWh" - assert not hasattr(tmp[0], "state_unit_of_measurement") - assert tmp[1].id == 2 - assert tmp[1].statistic_id == "test:fossil_percentage" - assert tmp[1].unit_of_measurement == "%" - assert not hasattr(tmp[1], "state_unit_of_measurement") - - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - # Test that the state_unit column is initialized during migration from schema 28 - hass = get_test_home_assistant() - recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - hass.start() - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - tmp = session.query(recorder.db_schema.StatisticsMeta).all() - assert len(tmp) == 2 - assert tmp[0].id == 1 - assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" - assert tmp[0].unit_of_measurement == "kWh" - assert hasattr(tmp[0], "state_unit_of_measurement") - assert tmp[0].state_unit_of_measurement == "kWh" - assert tmp[1].id == 2 - assert tmp[1].statistic_id == "test:fossil_percentage" - assert hasattr(tmp[1], "state_unit_of_measurement") - assert tmp[1].state_unit_of_measurement == "%" - assert tmp[1].state_unit_of_measurement == "%" - - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - def test_invalid_update(hass): """Test that an invalid new version raises an exception.""" with pytest.raises(ValueError): diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index c96b984bcf4..6d96b97b89c 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -159,7 +159,6 @@ def mock_sensor_statistics(): "has_mean": True, "has_sum": False, "name": None, - "state_unit_of_measurement": "dogs", "statistic_id": entity_id, "unit_of_measurement": "dogs", }, @@ -488,7 +487,6 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", } @@ -530,7 +528,6 @@ async def test_import_statistics( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -544,7 +541,6 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -604,7 +600,7 @@ async def test_import_statistics( ] } - # Update the previously inserted statistics + rename and change display unit + # Update the previously inserted statistics + rename external_statistics = { "start": period1, "max": 1, @@ -615,7 +611,6 @@ async def test_import_statistics( "sum": 5, } external_metadata["name"] = "Total imported energy renamed" - external_metadata["state_unit_of_measurement"] = "MWh" import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) @@ -626,7 +621,6 @@ async def test_import_statistics( "statistic_id": statistic_id, "name": "Total imported energy renamed", "source": source, - "state_unit_of_measurement": "MWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -640,7 +634,6 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy renamed", "source": source, - "state_unit_of_measurement": "MWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -653,12 +646,12 @@ async def test_import_statistics( "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), - "max": approx(1.0 / 1000), - "mean": approx(2.0 / 1000), - "min": approx(3.0 / 1000), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), "last_reset": last_reset_utc_str, - "state": approx(4.0 / 1000), - "sum": approx(5.0 / 1000), + "state": approx(4.0), + "sum": approx(5.0), }, { "statistic_id": statistic_id, @@ -668,13 +661,13 @@ async def test_import_statistics( "mean": None, "min": None, "last_reset": last_reset_utc_str, - "state": approx(1.0 / 1000), - "sum": approx(3.0 / 1000), + "state": approx(1.0), + "sum": approx(3.0), }, ] } - # Adjust the statistics + # Adjust the statistics in a different unit await client.send_json( { "id": 1, @@ -696,12 +689,12 @@ async def test_import_statistics( "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), - "max": approx(1.0 / 1000), - "mean": approx(2.0 / 1000), - "min": approx(3.0 / 1000), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), "last_reset": last_reset_utc_str, - "state": approx(4.0 / 1000), - "sum": approx(5.0 / 1000), + "state": approx(4.0), + "sum": approx(5.0), }, { "statistic_id": statistic_id, @@ -711,8 +704,8 @@ async def test_import_statistics( "mean": None, "min": None, "last_reset": last_reset_utc_str, - "state": approx(1.0 / 1000), - "sum": approx(1000 + 3.0 / 1000), + "state": approx(1.0), + "sum": approx(1000 * 1000 + 3.0), }, ] } @@ -741,7 +734,6 @@ def test_external_statistics_errors(hass_recorder, caplog): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import", "unit_of_measurement": "kWh", } @@ -805,16 +797,6 @@ def test_external_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} - # Attempt to insert statistics with an invalid unit combination - external_metadata = {**_external_metadata, "state_unit_of_measurement": "cats"} - external_statistics = {**_external_statistics} - with pytest.raises(HomeAssistantError): - async_add_external_statistics(hass, external_metadata, (external_statistics,)) - wait_recording_done(hass) - assert statistics_during_period(hass, zero, period="hour") == {} - assert list_statistic_ids(hass) == [] - assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} - def test_import_statistics_errors(hass_recorder, caplog): """Test validation of imported statistics.""" @@ -839,7 +821,6 @@ def test_import_statistics_errors(hass_recorder, caplog): "has_sum": True, "name": "Total imported energy", "source": "recorder", - "state_unit_of_measurement": "kWh", "statistic_id": "sensor.total_energy_import", "unit_of_measurement": "kWh", } @@ -903,16 +884,6 @@ def test_import_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} - # Attempt to insert statistics with an invalid unit combination - external_metadata = {**_external_metadata, "state_unit_of_measurement": "cats"} - external_statistics = {**_external_statistics} - with pytest.raises(HomeAssistantError): - async_import_statistics(hass, external_metadata, (external_statistics,)) - wait_recording_done(hass) - assert statistics_during_period(hass, zero, period="hour") == {} - assert list_statistic_ids(hass) == [] - assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} - @pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"]) @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") @@ -962,7 +933,6 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import", "unit_of_measurement": "kWh", } @@ -1081,7 +1051,6 @@ def test_duplicate_statistics_handle_integrity_error(hass_recorder, caplog): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 4d4a1604a91..58893ee3bb1 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -651,7 +651,6 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -673,7 +672,6 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -698,7 +696,6 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -719,7 +716,6 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -907,7 +903,6 @@ async def test_update_statistics_metadata( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -935,7 +930,6 @@ async def test_update_statistics_metadata( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": new_unit, "unit_class": new_unit_class, } @@ -999,7 +993,6 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -1055,7 +1048,6 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "W", "unit_class": "power", } @@ -1108,7 +1100,6 @@ async def test_change_statistics_unit_errors( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -1461,7 +1452,6 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": unit, "statistic_id": "test:total_gas", "unit_of_measurement": unit, } @@ -1487,7 +1477,6 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": unit, "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1515,7 +1504,6 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": attributes["unit_of_measurement"], "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1543,7 +1531,6 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": attributes["unit_of_measurement"], "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1640,7 +1627,6 @@ async def test_import_statistics( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1654,7 +1640,6 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -1869,7 +1854,6 @@ async def test_adjust_sum_statistics_energy( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1883,7 +1867,6 @@ async def test_adjust_sum_statistics_energy( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -2067,7 +2050,6 @@ async def test_adjust_sum_statistics_gas( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "m³", "statistics_unit_of_measurement": "m³", "unit_class": "volume", } @@ -2081,7 +2063,6 @@ async def test_adjust_sum_statistics_gas( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "m³", "statistic_id": statistic_id, "unit_of_measurement": "m³", }, @@ -2281,7 +2262,6 @@ async def test_adjust_sum_statistics_errors( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, } @@ -2295,7 +2275,6 @@ async def test_adjust_sum_statistics_errors( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": state_unit, "statistic_id": statistic_id, "unit_of_measurement": statistic_unit, }, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index f0013874e23..637d17e21a8 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -140,7 +140,6 @@ def test_compile_hourly_statistics( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -215,7 +214,6 @@ def test_compile_hourly_statistics_purged_state_changes( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -285,7 +283,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, @@ -295,7 +292,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, @@ -305,7 +301,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, @@ -440,7 +435,6 @@ async def test_compile_hourly_sum_statistics_amount( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -633,7 +627,6 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -734,7 +727,6 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -819,7 +811,6 @@ def test_compile_hourly_sum_statistics_nan_inf_state( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -933,7 +924,6 @@ def test_compile_hourly_sum_statistics_negative_state( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistic_id": entity_id, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, @@ -1022,7 +1012,6 @@ def test_compile_hourly_sum_statistics_total_no_reset( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1125,7 +1114,6 @@ def test_compile_hourly_sum_statistics_total_increasing( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1239,7 +1227,6 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1334,7 +1321,6 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1427,7 +1413,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, @@ -1437,7 +1422,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, @@ -1447,7 +1431,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "Wh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, @@ -1811,7 +1794,6 @@ def test_list_statistic_ids( "has_sum": statistic_type == "sum", "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1826,7 +1808,6 @@ def test_list_statistic_ids( "has_sum": statistic_type == "sum", "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1917,7 +1898,6 @@ def test_compile_hourly_statistics_changing_units_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1953,7 +1933,6 @@ def test_compile_hourly_statistics_changing_units_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2029,7 +2008,6 @@ def test_compile_hourly_statistics_changing_units_2( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "cats", "statistics_unit_of_measurement": "cats", "unit_class": unit_class, }, @@ -2095,7 +2073,6 @@ def test_compile_hourly_statistics_changing_units_3( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2131,7 +2108,6 @@ def test_compile_hourly_statistics_changing_units_3( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2197,7 +2173,6 @@ def test_compile_hourly_statistics_changing_device_class_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2243,7 +2218,6 @@ def test_compile_hourly_statistics_changing_device_class_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2306,7 +2280,6 @@ def test_compile_hourly_statistics_changing_device_class_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2386,7 +2359,6 @@ def test_compile_hourly_statistics_changing_device_class_2( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, }, @@ -2436,7 +2408,6 @@ def test_compile_hourly_statistics_changing_device_class_2( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, }, @@ -2506,7 +2477,6 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": None, "statistics_unit_of_measurement": None, "unit_class": None, }, @@ -2520,7 +2490,6 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": None, "statistic_id": "sensor.test1", "unit_of_measurement": None, }, @@ -2543,7 +2512,6 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": None, "statistics_unit_of_measurement": None, "unit_class": None, }, @@ -2557,7 +2525,6 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": None, "statistic_id": "sensor.test1", "unit_of_measurement": None, }, @@ -2738,7 +2705,6 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, @@ -2748,7 +2714,6 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, @@ -2758,7 +2723,6 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, @@ -2768,7 +2732,6 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "EUR", "statistics_unit_of_measurement": "EUR", "unit_class": None, }, From 321da50a7e735aed6511c1acfd2b5f033461098a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 1 Oct 2022 18:59:10 -0700 Subject: [PATCH 067/183] Update nest climate to avoid duplicate set mode commands (#79445) --- homeassistant/components/nest/climate_sdm.py | 2 ++ tests/components/nest/test_climate_sdm.py | 23 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index e40db60d5ed..dd257bb9301 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -315,6 +315,8 @@ class ThermostatEntity(ClimateEntity): """Set new target preset mode.""" if preset_mode not in self.preset_modes: raise ValueError(f"Unsupported preset_mode '{preset_mode}'") + if self.preset_mode == preset_mode: # API doesn't like duplicate preset modes + return trait = self._device.traits[ThermostatEcoTrait.NAME] try: await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 440855f6ab7..b6992a5772f 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -598,6 +598,29 @@ async def test_thermostat_set_eco_preset( "params": {"mode": "OFF"}, } + # Simulate the mode changing + await create_event( + { + "sdm.devices.traits.ThermostatEco": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + } + ) + + auth.method = None + auth.url = None + auth.json = None + + # Attempting to set the preset mode when already in that mode will + # not send any messages to the API (it would otherwise fail) + await common.async_set_preset_mode(hass, PRESET_NONE) + await hass.async_block_till_done() + + assert auth.method is None + assert auth.url is None + assert auth.json is None + async def test_thermostat_set_cool( hass: HomeAssistant, From 9bb75bb72646fafc40c878926e91d66fe1039677 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Sun, 2 Oct 2022 09:47:07 -0700 Subject: [PATCH 068/183] Skip parsing Flume sensors without location (#79456) --- homeassistant/components/flume/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index b9b5f819520..6d68058732d 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -40,7 +40,10 @@ async def async_setup_entry( flume_entity_list = [] for device in flume_devices.device_list: - if device[KEY_DEVICE_TYPE] != FLUME_TYPE_SENSOR: + if ( + device[KEY_DEVICE_TYPE] != FLUME_TYPE_SENSOR + or KEY_DEVICE_LOCATION not in device + ): continue device_id = device[KEY_DEVICE_ID] From 412ef9d126577f5f66e117a811749371eb37daf4 Mon Sep 17 00:00:00 2001 From: zbeky <32236798+zbeky@users.noreply.github.com> Date: Sun, 2 Oct 2022 20:34:53 +0200 Subject: [PATCH 069/183] Add EVOLVEO Heat M30v2 TRV (#79462) --- homeassistant/components/zha/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index ca3110dad60..a4e1be78c08 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -585,6 +585,7 @@ class CentralitePearl(ZenWithinThermostat): "_TZE200_4eeyebrt", "_TZE200_cpmgn2cf", "_TZE200_9sfg7gm0", + "_TZE200_8whxpsiw", "_TYST11_ckud7u2l", "_TYST11_ywdxldoj", "_TYST11_cwnjrr72", From f8f3d96a74c15771b04c511d710fc3cb03fb1776 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Oct 2022 06:46:01 -1000 Subject: [PATCH 070/183] Bump dbus-fast to 1.20.0 (#79465) --- 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 9c989bfe9fa..8aef8bb42c0 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.5.3", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.18.0" + "dbus-fast==1.20.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 258776dde30..b68772c9fa1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.18.0 +dbus-fast==1.20.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 314ee14c434..0c08873711e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.18.0 +dbus-fast==1.20.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71c241214e3..7808fa137ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.18.0 +dbus-fast==1.20.0 # homeassistant.components.debugpy debugpy==1.6.3 From c32e4d34f630d1489770442b2a212516f2dc6b14 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 2 Oct 2022 11:07:19 -0400 Subject: [PATCH 071/183] Remove unnecessary config entity from ZHA (#79472) --- .../zha/core/channels/manufacturerspecific.py | 30 ------------------- homeassistant/components/zha/number.py | 13 -------- 2 files changed, 43 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 49f5d1df249..724a794007d 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -181,43 +181,13 @@ class InovelliConfigEntityChannel(ZigbeeChannel): "power_type": False, "switch_type": False, "button_delay": False, - "device_bind_number": False, "smart_bulb_mode": False, "double_tap_up_for_full_brightness": False, - "default_led1_strip_color_when_on": False, - "default_led1_strip_color_when_off": False, - "default_led1_strip_intensity_when_on": False, - "default_led1_strip_intensity_when_off": False, - "default_led2_strip_color_when_on": False, - "default_led2_strip_color_when_off": False, - "default_led2_strip_intensity_when_on": False, - "default_led2_strip_intensity_when_off": False, - "default_led3_strip_color_when_on": False, - "default_led3_strip_color_when_off": False, - "default_led3_strip_intensity_when_on": False, - "default_led3_strip_intensity_when_off": False, - "default_led4_strip_color_when_on": False, - "default_led4_strip_color_when_off": False, - "default_led4_strip_intensity_when_on": False, - "default_led4_strip_intensity_when_off": False, - "default_led5_strip_color_when_on": False, - "default_led5_strip_color_when_off": False, - "default_led5_strip_intensity_when_on": False, - "default_led5_strip_intensity_when_off": False, - "default_led6_strip_color_when_on": False, - "default_led6_strip_color_when_off": False, - "default_led6_strip_intensity_when_on": False, - "default_led6_strip_intensity_when_off": False, - "default_led7_strip_color_when_on": False, - "default_led7_strip_color_when_off": False, - "default_led7_strip_intensity_when_on": False, - "default_led7_strip_intensity_when_off": False, "led_color_when_on": False, "led_color_when_off": False, "led_intensity_when_on": False, "led_intensity_when_off": False, "local_protection": False, - "remote_protection": False, "output_mode": False, "on_off_led_mode": False, "firmware_progress_led": False, diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 6fe411abfb3..3bace412744 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -576,19 +576,6 @@ class InovelliButtonDelay(ZHANumberConfigurationEntity, id_suffix="button_delay" _attr_name: str = "Button delay" -@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI) -class InovelliDeviceBindNumber( - ZHANumberConfigurationEntity, id_suffix="device_bind_number" -): - """Inovelli device bind number configuration entity.""" - - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 255 - _zcl_attribute: str = "device_bind_number" - _attr_name: str = "Device bind number" - - @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI) class InovelliLocalDimmingUpSpeed( ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_local" From 70010c0115f0a19ab5bb2a53cc2be6195866a20c Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Sun, 2 Oct 2022 21:14:02 -0400 Subject: [PATCH 072/183] Fix LaCrosse View not updating (#79474) --- homeassistant/components/lacrosse_view/sensor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 1ff3e78812f..684ac884345 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -105,7 +105,7 @@ async def async_setup_entry( sensors: list[Sensor] = coordinator.data sensor_list = [] - for sensor in sensors: + for i, sensor in enumerate(sensors): for field in sensor.sensor_field_names: description = SENSOR_DESCRIPTIONS.get(field) if description is None: @@ -125,6 +125,7 @@ async def async_setup_entry( coordinator=coordinator, description=description, sensor=sensor, + index=i, ) ) @@ -144,6 +145,7 @@ class LaCrosseViewSensor( description: LaCrosseSensorEntityDescription, coordinator: DataUpdateCoordinator[list[Sensor]], sensor: Sensor, + index: int, ) -> None: """Initialize.""" super().__init__(coordinator) @@ -157,11 +159,11 @@ class LaCrosseViewSensor( "model": sensor.model, "via_device": (DOMAIN, sensor.location.id), } - self._sensor = sensor + self.index = index @property def native_value(self) -> float | str: """Return the sensor value.""" return self.entity_description.value_fn( - self._sensor, self.entity_description.key + self.coordinator.data[self.index], self.entity_description.key ) From 43891f0baa02e6e207361431b75785665fc8e45b Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 2 Oct 2022 20:34:15 +0200 Subject: [PATCH 073/183] Fix empty default ZHA configuration (#79475) * Also add 0 as a default for transition in const.py This is the same default transition (none) that is used in ZHA's light.py * Send default values for unconfigured options in ZHA's configuration API * Remove options that match defaults values before saving --- homeassistant/components/zha/api.py | 22 ++++++++++++++++++++++ homeassistant/components/zha/core/const.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 1095bae5ac8..6cbcdf50983 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1028,6 +1028,12 @@ async def websocket_get_configuration( data["data"][section] = zha_gateway.config_entry.options.get( CUSTOM_CONFIGURATION, {} ).get(section, {}) + + # send default values for unconfigured options + for entry in data["schemas"][section]: + if data["data"][section].get(entry["name"]) is None: + data["data"][section][entry["name"]] = entry["default"] + connection.send_result(msg[ID], data) @@ -1047,6 +1053,22 @@ async def websocket_update_zha_configuration( options = zha_gateway.config_entry.options data_to_save = {**options, **{CUSTOM_CONFIGURATION: msg["data"]}} + for section, schema in ZHA_CONFIG_SCHEMAS.items(): + for entry in schema.schema: + # remove options that match defaults + if ( + data_to_save[CUSTOM_CONFIGURATION].get(section, {}).get(entry) + == entry.default() + ): + data_to_save[CUSTOM_CONFIGURATION][section].pop(entry) + # remove entire section block if empty + if not data_to_save[CUSTOM_CONFIGURATION][section]: + data_to_save[CUSTOM_CONFIGURATION].pop(section) + + # remove entire custom_configuration block if empty + if not data_to_save[CUSTOM_CONFIGURATION]: + data_to_save.pop(CUSTOM_CONFIGURATION) + _LOGGER.info( "Updating ZHA custom configuration options from %s to %s", options, diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 0204fb50bed..b9871a1f2ab 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -146,7 +146,7 @@ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { - vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, + vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION, default=0): cv.positive_int, vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean, vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, From bcd9510733c9d53d45b64ba0f44c3a712d696817 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 2 Oct 2022 20:50:01 +0200 Subject: [PATCH 074/183] Fix missing string message in UniFi (#79487) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 9e8a1ef28f3..0ff781418d4 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==35"], + "requirements": ["aiounifi==36"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0c08873711e..d45bcef0100 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==35 +aiounifi==36 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7808fa137ff..00f1854cf0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -251,7 +251,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==35 +aiounifi==36 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From cc8267fb13fb68339f15a9a0114e53841cb526be Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 2 Oct 2022 21:30:54 +0200 Subject: [PATCH 075/183] Update frontend to 20221002.0 (#79491) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index bcc574a4cad..3dbf73bdeaf 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220929.0"], + "requirements": ["home-assistant-frontend==20221002.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b68772c9fa1..a6c3fcceedd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.20.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220929.0 +home-assistant-frontend==20221002.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index d45bcef0100..7aba9d2ba79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,7 +865,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20220929.0 +home-assistant-frontend==20221002.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00f1854cf0b..91658b74bbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -645,7 +645,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20220929.0 +home-assistant-frontend==20221002.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 12fc7f29d5e0390487fb47a21a8d4172fe2420e9 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 3 Oct 2022 03:19:37 +0200 Subject: [PATCH 076/183] Set Synology DSM update entity to unavailable in case no data from api gathered (#79508) --- homeassistant/components/synology_dsm/update.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index d3f3cc56eac..445e682651c 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -52,6 +52,11 @@ class SynoDSMUpdateEntity(SynologyDSMBaseEntity, UpdateEntity): entity_description: SynologyDSMUpdateEntityEntityDescription _attr_title = "Synology DSM" + @property + def available(self) -> bool: + """Return True if entity is available.""" + return bool(self._api.upgrade) + @property def installed_version(self) -> str | None: """Version installed and in use.""" From 8c84efa842be304e9f052ae864892dcbac6aee31 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 3 Oct 2022 03:11:45 +0200 Subject: [PATCH 077/183] Remove deprecated update binary sensor from Synology DSM (#79509) --- .../components/synology_dsm/binary_sensor.py | 49 +------------------ 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index b5f5effbb8e..ac930467442 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -1,12 +1,10 @@ """Support for Synology DSM binary sensors.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass from typing import Any from synology_dsm.api.core.security import SynoCoreSecurity -from synology_dsm.api.core.upgrade import SynoCoreUpgrade from synology_dsm.api.storage.storage import SynoStorage from homeassistant.components.binary_sensor import ( @@ -38,18 +36,6 @@ class SynologyDSMBinarySensorEntityDescription( """Describes Synology DSM binary sensor entity.""" -UPGRADE_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = ( - SynologyDSMBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 (#68664) - api_key=SynoCoreUpgrade.API_KEY, - key="update_available", - name="Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - entity_category=EntityCategory.DIAGNOSTIC, - ), -) - SECURITY_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = ( SynologyDSMBinarySensorEntityDescription( api_key=SynoCoreSecurity.API_KEY, @@ -85,22 +71,11 @@ async def async_setup_entry( api = data.api coordinator = data.coordinator_central - entities: list[ - SynoDSMSecurityBinarySensor - | SynoDSMUpgradeBinarySensor - | SynoDSMStorageBinarySensor - ] = [ + entities: list[SynoDSMSecurityBinarySensor | SynoDSMStorageBinarySensor] = [ SynoDSMSecurityBinarySensor(api, coordinator, description) for description in SECURITY_BINARY_SENSORS ] - entities.extend( - [ - SynoDSMUpgradeBinarySensor(api, coordinator, description) - for description in UPGRADE_BINARY_SENSORS - ] - ) - # Handle all disks if api.storage.disks_ids: entities.extend( @@ -169,25 +144,3 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, SynoDSMBinarySensor): return bool( getattr(self._api.storage, self.entity_description.key)(self._device_id) ) - - -class SynoDSMUpgradeBinarySensor(SynoDSMBinarySensor): - """Representation a Synology Upgrade binary sensor.""" - - @property - def is_on(self) -> bool: - """Return the state.""" - return bool(getattr(self._api.upgrade, self.entity_description.key)) - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return bool(self._api.upgrade) - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return firmware details.""" - return { - "installed_version": self._api.information.version_string, - "latest_available_version": self._api.upgrade.available_version, - } From ad1ed811e8d0a2598907f73a41117ce6e97b76ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Oct 2022 15:12:14 -1000 Subject: [PATCH 078/183] Bump bluetooth dependencies (#79514) --- homeassistant/components/bluetooth/manifest.json | 4 ++-- homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 8aef8bb42c0..13fe28a5b1e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -8,9 +8,9 @@ "requirements": [ "bleak==0.18.1", "bleak-retry-connector==2.1.3", - "bluetooth-adapters==0.5.3", + "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.20.0" + "dbus-fast==1.21.17" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a6c3fcceedd..a58e32ca8e4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,12 +12,12 @@ awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.1.3 bleak==0.18.1 -bluetooth-adapters==0.5.3 +bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.20.0 +dbus-fast==1.21.17 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7aba9d2ba79..96b239cd4fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -435,7 +435,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.3 +bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.20.0 +dbus-fast==1.21.17 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91658b74bbc..e2acc3b2092 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,7 +349,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.3 +bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.20.0 +dbus-fast==1.21.17 # homeassistant.components.debugpy debugpy==1.6.3 From 68aa0856c33c68f47a1e507bea32513c30ac8e8b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 3 Oct 2022 14:11:18 +1300 Subject: [PATCH 079/183] Bump aioesphomeapi to 11.1.0 (#79515) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c6a475b6eea..066050d796d 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.0.0"], + "requirements": ["aioesphomeapi==11.1.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 96b239cd4fb..1b02251adf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.0.0 +aioesphomeapi==11.1.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2acc3b2092..614464f1d1f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.0.0 +aioesphomeapi==11.1.0 # homeassistant.components.flo aioflo==2021.11.0 From 3abd0877f7dee566a0e7668b4783eb1b065f569e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Oct 2022 21:25:55 -0400 Subject: [PATCH 080/183] Bumped version to 2022.10.0b4 --- 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 1415a720c83..6bb835149b1 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 = 10 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __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 464cefbda94..f48fb8bed76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0b3" +version = "2022.10.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8f44c11677f9ab18b63cfdfec7476025115357ed Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 3 Oct 2022 18:10:28 -0700 Subject: [PATCH 081/183] Add option to set a stun server for RTSPtoWebRTC (#72574) --- .../components/rtsp_to_webrtc/__init__.py | 35 +++++++++- .../components/rtsp_to_webrtc/config_flow.py | 42 ++++++++++- .../components/rtsp_to_webrtc/strings.json | 9 +++ .../rtsp_to_webrtc/translations/en.json | 9 +++ tests/components/rtsp_to_webrtc/conftest.py | 15 +++- .../rtsp_to_webrtc/test_config_flow.py | 46 +++++++++++++ tests/components/rtsp_to_webrtc/test_init.py | 69 ++++++++++++++++++- 7 files changed, 219 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rtsp_to_webrtc/__init__.py b/homeassistant/components/rtsp_to_webrtc/__init__.py index 185cfcb0240..f0e013fc02f 100644 --- a/homeassistant/components/rtsp_to_webrtc/__init__.py +++ b/homeassistant/components/rtsp_to_webrtc/__init__.py @@ -24,10 +24,11 @@ import async_timeout from rtsp_to_webrtc.client import get_adaptive_client from rtsp_to_webrtc.exceptions import ClientError, ResponseError from rtsp_to_webrtc.interface import WebRTCClientInterface +import voluptuous as vol -from homeassistant.components import camera +from homeassistant.components import camera, websocket_api from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -37,6 +38,7 @@ DOMAIN = "rtsp_to_webrtc" DATA_SERVER_URL = "server_url" DATA_UNSUB = "unsub" TIMEOUT = 10 +CONF_STUN_SERVER = "stun_server" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -54,6 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (TimeoutError, ClientError) as err: raise ConfigEntryNotReady from err + hass.data[DOMAIN][CONF_STUN_SERVER] = entry.options.get(CONF_STUN_SERVER, "") + async def async_offer_for_stream_source( stream_source: str, offer_sdp: str, @@ -78,10 +82,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, async_offer_for_stream_source ) ) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + + websocket_api.async_register_command(hass, ws_get_settings) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if DOMAIN in hass.data: + del hass.data[DOMAIN] return True + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload config entry when options change.""" + if hass.data[DOMAIN][CONF_STUN_SERVER] != entry.options.get(CONF_STUN_SERVER, ""): + await hass.config_entries.async_reload(entry.entry_id) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "rtsp_to_webrtc/get_settings", + } +) +@callback +def ws_get_settings( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle the websocket command.""" + connection.send_result( + msg["id"], + {CONF_STUN_SERVER: hass.data.get(DOMAIN, {}).get(CONF_STUN_SERVER, "")}, + ) diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py index 815c5e5db7b..865a6bafcb6 100644 --- a/homeassistant/components/rtsp_to_webrtc/config_flow.py +++ b/homeassistant/components/rtsp_to_webrtc/config_flow.py @@ -11,10 +11,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from . import DATA_SERVER_URL, DOMAIN +from . import CONF_STUN_SERVER, DATA_SERVER_URL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -104,3 +105,42 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=self._hassio_discovery["addon"], data={DATA_SERVER_URL: url}, ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Create an options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """RTSPtoWeb Options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_STUN_SERVER, + description={ + "suggested_value": self.config_entry.options.get( + CONF_STUN_SERVER + ), + }, + ): str, + } + ), + ) diff --git a/homeassistant/components/rtsp_to_webrtc/strings.json b/homeassistant/components/rtsp_to_webrtc/strings.json index 5ef91eaf206..939c30766e2 100644 --- a/homeassistant/components/rtsp_to_webrtc/strings.json +++ b/homeassistant/components/rtsp_to_webrtc/strings.json @@ -23,5 +23,14 @@ "server_failure": "RTSPtoWebRTC server returned an error. Check logs for more information.", "server_unreachable": "Unable to communicate with RTSPtoWebRTC server. Check logs for more information." } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun server address (host:port)" + } + } + } } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/en.json b/homeassistant/components/rtsp_to_webrtc/translations/en.json index c54983d63d3..a519883b764 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/en.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/en.json @@ -23,5 +23,14 @@ "title": "Configure RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun server address (host:port)" + } + } + } } } \ No newline at end of file diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index 5e737efc397..5a0d6de01df 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -65,9 +65,20 @@ async def config_entry_data() -> dict[str, Any]: @pytest.fixture -async def config_entry(config_entry_data: dict[str, Any]) -> MockConfigEntry: +def config_entry_options() -> dict[str, Any] | None: + """Fixture to set initial config entry options.""" + return None + + +@pytest.fixture +async def config_entry( + config_entry_data: dict[str, Any], + config_entry_options: dict[str, Any] | None, +) -> MockConfigEntry: """Fixture for MockConfigEntry.""" - return MockConfigEntry(domain=DOMAIN, data=config_entry_data) + return MockConfigEntry( + domain=DOMAIN, data=config_entry_data, options=config_entry_options + ) @pytest.fixture diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index a6cd4d6798f..cca6395c317 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -9,8 +9,11 @@ import rtsp_to_webrtc from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.rtsp_to_webrtc import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from .conftest import ComponentSetup + from tests.common import MockConfigEntry @@ -212,3 +215,46 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result.get("type") == "abort" assert result.get("reason") == "server_failure" + + +async def test_options_flow( + hass: HomeAssistant, + config_entry: MockConfigEntry, + setup_integration: ComponentSetup, +) -> None: + """Test setting stun server in options flow.""" + with patch( + "homeassistant.components.rtsp_to_webrtc.async_setup_entry", + return_value=True, + ): + await setup_integration() + + assert config_entry.state is ConfigEntryState.LOADED + assert not config_entry.options + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + data_schema = result["data_schema"].schema + assert set(data_schema) == {"stun_server"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "stun_server": "example.com:1234", + }, + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + assert config_entry.options == {"stun_server": "example.com:1234"} + + # Clear the value + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + assert config_entry.options == {} diff --git a/tests/components/rtsp_to_webrtc/test_init.py b/tests/components/rtsp_to_webrtc/test_init.py index 759fea7c813..afa365a3044 100644 --- a/tests/components/rtsp_to_webrtc/test_init.py +++ b/tests/components/rtsp_to_webrtc/test_init.py @@ -11,13 +11,14 @@ import aiohttp import pytest import rtsp_to_webrtc -from homeassistant.components.rtsp_to_webrtc import DOMAIN +from homeassistant.components.rtsp_to_webrtc import CONF_STUN_SERVER, DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from .conftest import SERVER_URL, STREAM_SOURCE, ComponentSetup +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker # The webrtc component does not inspect the details of the offer and answer, @@ -154,3 +155,69 @@ async def test_offer_failure( assert response["error"].get("code") == "web_rtc_offer_failed" assert "message" in response["error"] assert "RTSPtoWebRTC server communication failure" in response["error"]["message"] + + +async def test_no_stun_server( + hass: HomeAssistant, + rtsp_to_webrtc_client: Any, + setup_integration: ComponentSetup, + hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]], +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 2, + "type": "rtsp_to_webrtc/get_settings", + } + ) + response = await client.receive_json() + assert response.get("id") == 2 + assert response.get("type") == TYPE_RESULT + assert "result" in response + assert response["result"].get("stun_server") == "" + + +@pytest.mark.parametrize( + "config_entry_options", [{CONF_STUN_SERVER: "example.com:1234"}] +) +async def test_stun_server( + hass: HomeAssistant, + rtsp_to_webrtc_client: Any, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]], +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 3, + "type": "rtsp_to_webrtc/get_settings", + } + ) + response = await client.receive_json() + assert response.get("id") == 3 + assert response.get("type") == TYPE_RESULT + assert "result" in response + assert response["result"].get("stun_server") == "example.com:1234" + + # Simulate an options flow change, clearing the stun server and verify the change is reflected + hass.config_entries.async_update_entry(config_entry, options={}) + await hass.async_block_till_done() + + await client.send_json( + { + "id": 4, + "type": "rtsp_to_webrtc/get_settings", + } + ) + response = await client.receive_json() + assert response.get("id") == 4 + assert response.get("type") == TYPE_RESULT + assert "result" in response + assert response["result"].get("stun_server") == "" From b519bf533234aa8839568fe7f112210ed2e19c7a Mon Sep 17 00:00:00 2001 From: Ben Randall Date: Mon, 3 Oct 2022 10:42:57 -0400 Subject: [PATCH 082/183] Improve device_automation trigger validation (#75044) * improve device_automation trigger validation Validates the trigger configuration against the device_trigger schema before trying to access any of the properties in order to provide better error messages. Updates the error message to include an explicit indication that the error is coming from a trigger configuration. The inner error message from the validator can be accessed by viewing the stack trace. Add test case for trigger missing domain. Make action and condition validation consistent with trigger. This is not strictly necessary, but should be helpful for certain use cases that bypass some of the outer validation. Removed redundant schema elements from humidifier device_trigger. **Blueprint with missing `domain`** ``` 2022-07-12 06:02:18.351 ERROR (MainThread) [homeassistant.setup] Error during setup of component automation Traceback (most recent call last): File "/workspaces/core/homeassistant/setup.py", line 235, in _async_setup_component result = await task File "/workspaces/core/homeassistant/components/automation/__init__.py", line 241, in async_setup if not await _async_process_config(hass, config, component): File "/workspaces/core/homeassistant/components/automation/__init__.py", line 648, in _async_process_config await async_validate_config_item(hass, raw_config), File "/workspaces/core/homeassistant/components/automation/config.py", line 74, in async_validate_config_item config[CONF_TRIGGER] = await async_validate_trigger_config( File "/workspaces/core/homeassistant/helpers/trigger.py", line 59, in async_validate_trigger_config conf = await platform.async_validate_trigger_config(hass, conf) File "/workspaces/core/homeassistant/components/device_automation/trigger.py", line 67, in async_validate_trigger_config hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER KeyError: 'domain' ``` **Blueprint with missing `property` (specific to zwave_js event schema)** ``` 2022-07-12 06:09:54.206 ERROR (MainThread) [homeassistant.components.automation] Blueprint Missing Property generated invalid automation with inputs OrderedDict([('control_switch', '498be56d796836a67406e9ad373d23db')]): required key not provided @ data['property']. Got None ``` **Blueprint with missing `domain`** ``` 2022-07-12 06:12:16.080 ERROR (MainThread) [homeassistant.components.automation] Blueprint Missing Domain generated invalid automation with inputs OrderedDict([('control_switch', '498be56d796836a67406e9ad373d23db')]): invalid trigger configuration: required key not provided @ data['domain']. Got ``` **Blueprint with missing `property` (specific to zwave_js event schema)** ``` 2022-07-12 06:12:16.680 ERROR (MainThread) [homeassistant.components.automation] Blueprint Missing Property generated invalid automation with inputs OrderedDict([('control_switch', '498be56d796836a67406e9ad373d23db')]): invalid trigger configuration: required key not provided @ data['property']. Got ``` * Revert humifidier TRIGGER_SCHEMA change. --- .../components/device_automation/action.py | 6 ++- .../components/device_automation/condition.py | 4 +- .../components/device_automation/trigger.py | 5 +- .../components/rfxtrx/device_action.py | 1 - .../components/device_automation/test_init.py | 51 +++++++++++++++++-- .../components/webostv/test_device_trigger.py | 3 +- 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index 081b6bb283a..432ff2fdb7d 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from . import DeviceAutomationType, async_get_device_automation_platform @@ -51,14 +52,15 @@ async def async_validate_action_config( ) -> ConfigType: """Validate config.""" try: + config = cv.DEVICE_ACTION_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.ACTION ) if hasattr(platform, "async_validate_action_config"): return await platform.async_validate_action_config(hass, config) return cast(ConfigType, platform.ACTION_SCHEMA(config)) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid action configuration") from err + except (vol.Invalid, InvalidDeviceAutomationConfig) as err: + raise vol.Invalid("invalid action configuration: " + str(err)) from err async def async_call_action_from_config( diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index d656908f4be..3b0a5263f9e 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -58,8 +58,8 @@ async def async_validate_condition_config( if hasattr(platform, "async_validate_condition_config"): return await platform.async_validate_condition_config(hass, config) return cast(ConfigType, platform.CONDITION_SCHEMA(config)) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid condition configuration") from err + except (vol.Invalid, InvalidDeviceAutomationConfig) as err: + raise vol.Invalid("invalid condition configuration: " + str(err)) from err async def async_condition_from_config( diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index bd72b24d844..aac56b39846 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -58,14 +58,15 @@ async def async_validate_trigger_config( ) -> ConfigType: """Validate config.""" try: + config = TRIGGER_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER ) if not hasattr(platform, "async_validate_trigger_config"): return cast(ConfigType, platform.TRIGGER_SCHEMA(config)) return await platform.async_validate_trigger_config(hass, config) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid trigger configuration") from err + except (vol.Invalid, InvalidDeviceAutomationConfig) as err: + raise InvalidDeviceAutomationConfig("invalid trigger configuration") from err async def async_attach_trigger( diff --git a/homeassistant/components/rfxtrx/device_action.py b/homeassistant/components/rfxtrx/device_action.py index 15595b88cd2..7ea4ed07423 100644 --- a/homeassistant/components/rfxtrx/device_action.py +++ b/homeassistant/components/rfxtrx/device_action.py @@ -80,7 +80,6 @@ async def async_validate_action_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - config = ACTION_SCHEMA(config) commands, _ = _get_commands(hass, config[CONF_DEVICE_ID], config[CONF_TYPE]) sub_type = config[CONF_SUBTYPE] diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 3ead6fcb35d..71c062cf7d9 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -720,7 +720,28 @@ async def test_automation_with_bad_condition_action(hass, caplog): assert "required key not provided" in caplog.text -async def test_automation_with_bad_condition(hass, caplog): +async def test_automation_with_bad_condition_missing_domain(hass, caplog): + """Test automation with bad device condition.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": {"condition": "device", "device_id": "hello.device"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert ( + "Invalid config for [automation]: required key not provided @ data['condition'][0]['domain']" + in caplog.text + ) + + +async def test_automation_with_bad_condition_missing_device_id(hass, caplog): """Test automation with bad device condition.""" assert await async_setup_component( hass, @@ -735,7 +756,10 @@ async def test_automation_with_bad_condition(hass, caplog): }, ) - assert "required key not provided" in caplog.text + assert ( + "Invalid config for [automation]: required key not provided @ data['condition'][0]['device_id']" + in caplog.text + ) @pytest.fixture @@ -876,8 +900,25 @@ async def test_automation_with_bad_sub_condition(hass, caplog): assert "required key not provided" in caplog.text -async def test_automation_with_bad_trigger(hass, caplog): - """Test automation with bad device trigger.""" +async def test_automation_with_bad_trigger_missing_domain(hass, caplog): + """Test automation with device trigger this is missing domain.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "device", "device_id": "hello.device"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided @ data['domain']" in caplog.text + + +async def test_automation_with_bad_trigger_missing_device_id(hass, caplog): + """Test automation with device trigger that is missing device_id.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -890,7 +931,7 @@ async def test_automation_with_bad_trigger(hass, caplog): }, ) - assert "required key not provided" in caplog.text + assert "required key not provided @ data['device_id']" in caplog.text async def test_websocket_device_not_found(hass, hass_ws_client): diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index db15ce3a592..96914885971 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -128,8 +128,7 @@ async def test_get_triggers_for_invalid_device_id(hass, caplog): await hass.async_block_till_done() assert ( - "Invalid config for [automation]: Device invalid_device_id is not a valid webostv device" - in caplog.text + "Invalid config for [automation]: invalid trigger configuration" in caplog.text ) From e513c4fafc931238df825c17a756b35fe096aafc Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 3 Oct 2022 11:44:20 +0200 Subject: [PATCH 083/183] Bumb velbusaio to 2022.10.1 (#79471) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index cbc8db0ca9f..8b15dd1fa9f 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2022.9.1"], + "requirements": ["velbus-aio==2022.10.1"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index 1b02251adf2..1bb2b9c85cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2472,7 +2472,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.9.1 +velbus-aio==2022.10.1 # homeassistant.components.venstar venstarcolortouch==0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 614464f1d1f..00df6ffe3e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1706,7 +1706,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.9.1 +velbus-aio==2022.10.1 # homeassistant.components.venstar venstarcolortouch==0.18 From 736991af35f62dd90692d566dc9bad08bfea9f20 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Oct 2022 10:09:55 +0200 Subject: [PATCH 084/183] Align temperature conversion with other converters (#79521) * Align temperature conversion with other converters * Add comments and docstring * Align tests --- homeassistant/components/alexa/handlers.py | 4 +- homeassistant/util/temperature.py | 6 +- homeassistant/util/unit_conversion.py | 96 +++++++++------------- tests/util/test_unit_conversion.py | 5 +- 4 files changed, 48 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 73410ba06d2..b4c842dd5b5 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -819,7 +819,9 @@ def temperature_from_object(hass, temp_obj, interval=False): # convert to Celsius if absolute temperature temp -= 273.15 - return TemperatureConverter.convert(temp, from_unit, to_unit, interval=interval) + if interval: + return TemperatureConverter.convert_interval(temp, from_unit, to_unit) + return TemperatureConverter.convert(temp, from_unit, to_unit) @HANDLERS.register(("Alexa.ThermostatController", "SetTargetTemperature")) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index e08e1207e06..9173fbc5eee 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -43,6 +43,6 @@ def convert( "unit_conversion.TemperatureConverter instead", error_if_core=False, ) - return TemperatureConverter.convert( - temperature, from_unit, to_unit, interval=interval - ) + if interval: + return TemperatureConverter.convert_interval(temperature, from_unit, to_unit) + return TemperatureConverter.convert(temperature, from_unit, to_unit) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index cb066901b37..6d502ee6e6d 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -1,8 +1,6 @@ """Typing Helpers for Home Assistant.""" from __future__ import annotations -from abc import abstractmethod - from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, @@ -88,20 +86,6 @@ class BaseUnitConverter: NORMALIZED_UNIT: str VALID_UNITS: set[str] - @classmethod - @abstractmethod - def convert(cls, value: float, from_unit: str, to_unit: str) -> float: - """Convert one unit of measurement to another.""" - - @classmethod - @abstractmethod - def get_unit_ratio(cls, from_unit: str, to_unit: str) -> float: - """Get unit ratio between units of measurement.""" - - -class BaseUnitConverterWithUnitConversion(BaseUnitConverter): - """Define the format of a conversion utility.""" - _UNIT_CONVERSION: dict[str, float] @classmethod @@ -133,7 +117,7 @@ class BaseUnitConverterWithUnitConversion(BaseUnitConverter): return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit] -class DistanceConverter(BaseUnitConverterWithUnitConversion): +class DistanceConverter(BaseUnitConverter): """Utility to convert distance values.""" UNIT_CLASS = "distance" @@ -160,7 +144,7 @@ class DistanceConverter(BaseUnitConverterWithUnitConversion): } -class EnergyConverter(BaseUnitConverterWithUnitConversion): +class EnergyConverter(BaseUnitConverter): """Utility to convert energy values.""" UNIT_CLASS = "energy" @@ -177,7 +161,7 @@ class EnergyConverter(BaseUnitConverterWithUnitConversion): } -class MassConverter(BaseUnitConverterWithUnitConversion): +class MassConverter(BaseUnitConverter): """Utility to convert mass values.""" UNIT_CLASS = "mass" @@ -200,7 +184,7 @@ class MassConverter(BaseUnitConverterWithUnitConversion): } -class PowerConverter(BaseUnitConverterWithUnitConversion): +class PowerConverter(BaseUnitConverter): """Utility to convert power values.""" UNIT_CLASS = "power" @@ -215,7 +199,7 @@ class PowerConverter(BaseUnitConverterWithUnitConversion): } -class PressureConverter(BaseUnitConverterWithUnitConversion): +class PressureConverter(BaseUnitConverter): """Utility to convert pressure values.""" UNIT_CLASS = "pressure" @@ -244,7 +228,7 @@ class PressureConverter(BaseUnitConverterWithUnitConversion): } -class SpeedConverter(BaseUnitConverterWithUnitConversion): +class SpeedConverter(BaseUnitConverter): """Utility to convert speed values.""" UNIT_CLASS = "speed" @@ -281,47 +265,49 @@ class TemperatureConverter(BaseUnitConverter): TEMP_FAHRENHEIT, TEMP_KELVIN, } - _UNIT_RATIO = { + _UNIT_CONVERSION = { TEMP_CELSIUS: 1.0, TEMP_FAHRENHEIT: 1.8, TEMP_KELVIN: 1.0, } @classmethod - def convert( - cls, value: float, from_unit: str, to_unit: str, *, interval: bool = False - ) -> float: - """Convert a temperature from one unit to another.""" + def convert(cls, value: float, from_unit: str, to_unit: str) -> float: + """Convert a temperature from one unit to another. + + eg. 10°C will return 50°F + + For converting an interval between two temperatures, please use + `convert_interval` instead. + """ + # We cannot use the implementation from BaseUnitConverter here because the temperature + # units do not use the same floor: 0°C, 0°F and 0K do not align if from_unit == to_unit: return value if from_unit == TEMP_CELSIUS: if to_unit == TEMP_FAHRENHEIT: - return cls._celsius_to_fahrenheit(value, interval) + return cls._celsius_to_fahrenheit(value) if to_unit == TEMP_KELVIN: - return cls._celsius_to_kelvin(value, interval) + return cls._celsius_to_kelvin(value) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) if from_unit == TEMP_FAHRENHEIT: if to_unit == TEMP_CELSIUS: - return cls._fahrenheit_to_celsius(value, interval) + return cls._fahrenheit_to_celsius(value) if to_unit == TEMP_KELVIN: - return cls._celsius_to_kelvin( - cls._fahrenheit_to_celsius(value, interval), interval - ) + return cls._celsius_to_kelvin(cls._fahrenheit_to_celsius(value)) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) if from_unit == TEMP_KELVIN: if to_unit == TEMP_CELSIUS: - return cls._kelvin_to_celsius(value, interval) + return cls._kelvin_to_celsius(value) if to_unit == TEMP_FAHRENHEIT: - return cls._celsius_to_fahrenheit( - cls._kelvin_to_celsius(value, interval), interval - ) + return cls._celsius_to_fahrenheit(cls._kelvin_to_celsius(value)) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) @@ -330,40 +316,40 @@ class TemperatureConverter(BaseUnitConverter): ) @classmethod - def _fahrenheit_to_celsius(cls, fahrenheit: float, interval: bool = False) -> float: + def convert_interval(cls, interval: float, from_unit: str, to_unit: str) -> float: + """Convert a temperature interval from one unit to another. + + eg. a 10°C interval (10°C to 20°C) will return a 18°F (50°F to 68°F) interval + + For converting a temperature value, please use `convert` as this method + skips floor adjustment. + """ + # We use BaseUnitConverter implementation here because we are only interested + # in the ratio between the units. + return super().convert(interval, from_unit, to_unit) + + @classmethod + def _fahrenheit_to_celsius(cls, fahrenheit: float) -> float: """Convert a temperature in Fahrenheit to Celsius.""" - if interval: - return fahrenheit / 1.8 return (fahrenheit - 32.0) / 1.8 @classmethod - def _kelvin_to_celsius(cls, kelvin: float, interval: bool = False) -> float: + def _kelvin_to_celsius(cls, kelvin: float) -> float: """Convert a temperature in Kelvin to Celsius.""" - if interval: - return kelvin return kelvin - 273.15 @classmethod - def _celsius_to_fahrenheit(cls, celsius: float, interval: bool = False) -> float: + def _celsius_to_fahrenheit(cls, celsius: float) -> float: """Convert a temperature in Celsius to Fahrenheit.""" - if interval: - return celsius * 1.8 return celsius * 1.8 + 32.0 @classmethod - def _celsius_to_kelvin(cls, celsius: float, interval: bool = False) -> float: + def _celsius_to_kelvin(cls, celsius: float) -> float: """Convert a temperature in Celsius to Kelvin.""" - if interval: - return celsius return celsius + 273.15 - @classmethod - def get_unit_ratio(cls, from_unit: str, to_unit: str) -> float: - """Get unit ratio between units of measurement.""" - return cls._UNIT_RATIO[from_unit] / cls._UNIT_RATIO[to_unit] - -class VolumeConverter(BaseUnitConverterWithUnitConversion): +class VolumeConverter(BaseUnitConverter): """Utility to convert volume values.""" UNIT_CLASS = "volume" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index ca70af2e53f..ec839a6575c 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -452,10 +452,7 @@ def test_temperature_convert_with_interval( value: float, from_unit: str, expected: float, to_unit: str ) -> None: """Test conversion to other units.""" - assert ( - TemperatureConverter.convert(value, from_unit, to_unit, interval=True) - == expected - ) + assert TemperatureConverter.convert_interval(value, from_unit, to_unit) == expected @pytest.mark.parametrize( From d77afb0a791f0da18687e93abac5a84419124020 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Oct 2022 03:11:51 -1000 Subject: [PATCH 085/183] Bump dbus-fast to 1.22.0 (#79527) Performance improvements https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.21.17...v1.22.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 13fe28a5b1e..1cb01a7da63 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.21.17" + "dbus-fast==1.22.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a58e32ca8e4..a8b011a1a85 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.21.17 +dbus-fast==1.22.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1bb2b9c85cf..14533e66d4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.21.17 +dbus-fast==1.22.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00df6ffe3e1..4135782817b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.21.17 +dbus-fast==1.22.0 # homeassistant.components.debugpy debugpy==1.6.3 From d32ab6ff8f8080446bf7fa7f0f47c81e53bd1844 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 3 Oct 2022 16:17:08 +0200 Subject: [PATCH 086/183] Bump velbusaio to 2022.10.2 (#79537) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 8b15dd1fa9f..1a5d78d24d6 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2022.10.1"], + "requirements": ["velbus-aio==2022.10.2"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index 14533e66d4c..8fbbff6b1e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2472,7 +2472,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.1 +velbus-aio==2022.10.2 # homeassistant.components.venstar venstarcolortouch==0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4135782817b..cfb010389c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1706,7 +1706,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.1 +velbus-aio==2022.10.2 # homeassistant.components.venstar venstarcolortouch==0.18 From a9f2119932f92fd7ee2d9064522d5a08f0de264e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 Oct 2022 21:45:28 +0200 Subject: [PATCH 087/183] Update frontend to 20221003.0 (#79551) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 3dbf73bdeaf..fde637657dd 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221002.0"], + "requirements": ["home-assistant-frontend==20221003.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a8b011a1a85..aeb65b379ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.22.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221002.0 +home-assistant-frontend==20221003.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 8fbbff6b1e1..5c02ca53814 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,7 +865,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221002.0 +home-assistant-frontend==20221003.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfb010389c1..22a10dbf4b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -645,7 +645,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221002.0 +home-assistant-frontend==20221003.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 85c131ed43b9c5e83c7442940d38c671677cfa20 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Oct 2022 03:13:48 +0200 Subject: [PATCH 088/183] Fix preserving long term statistics when entity_id is changed (#79556) --- homeassistant/components/recorder/statistics.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index ef066b82060..5ecf1dc1e44 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -29,6 +29,7 @@ from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import dt as dt_util @@ -296,13 +297,17 @@ def async_setup(hass: HomeAssistant) -> None: return True - if hass.is_running: + @callback + def setup_entity_registry_event_handler(hass: HomeAssistant) -> None: + """Subscribe to event registry events.""" hass.bus.async_listen( entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_id_changed, event_filter=entity_registry_changed_filter, ) + async_at_start(hass, setup_entity_registry_event_handler) + def get_start_time() -> datetime: """Return start time.""" From b1883609cfb9c69ff19d53be82a2836dc325d027 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Oct 2022 15:15:09 -1000 Subject: [PATCH 089/183] Remove call to deprecated bleak register_detection_callback (#79558) --- .../components/bluetooth/__init__.py | 6 ++--- homeassistant/components/bluetooth/scanner.py | 24 +++++++++++++------ tests/components/bluetooth/test_init.py | 4 +++- tests/components/bluetooth/test_scanner.py | 14 ++++------- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 2afb638b230..f175b01b798 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -55,7 +55,7 @@ from .models import ( HaBluetoothConnector, ProcessAdvertisementCallback, ) -from .scanner import HaScanner, ScannerStartError, create_bleak_scanner +from .scanner import HaScanner, ScannerStartError from .util import adapter_human_name, adapter_unique_name, async_default_adapter if TYPE_CHECKING: @@ -400,13 +400,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: passive = entry.options.get(CONF_PASSIVE) mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE + scanner = HaScanner(hass, mode, adapter, address) try: - bleak_scanner = create_bleak_scanner(mode, adapter) + scanner.async_setup() except RuntimeError as err: raise ConfigEntryNotReady( f"{adapter_human_name(adapter, address)}: {err}" ) from err - scanner = HaScanner(hass, bleak_scanner, adapter, address) info_callback = async_get_advertisement_callback(hass) entry.async_on_unload(scanner.async_register_callback(info_callback)) try: diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 857a0e4c01c..9bc68059a7f 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -16,7 +16,7 @@ from bleak.assigned_numbers import AdvertisementDataType from bleak.backends.bluezdbus.advertisement_monitor import OrPattern from bleak.backends.bluezdbus.scanner import BlueZScannerArgs from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData +from bleak.backends.scanner import AdvertisementData, AdvertisementDataCallback from bleak_retry_connector import get_device_by_adapter from dbus_fast import InvalidMessageError @@ -86,11 +86,14 @@ class ScannerStartError(HomeAssistantError): def create_bleak_scanner( - scanning_mode: BluetoothScanningMode, adapter: str | None + detection_callback: AdvertisementDataCallback, + scanning_mode: BluetoothScanningMode, + adapter: str | None, ) -> bleak.BleakScanner: """Create a Bleak scanner.""" scanner_kwargs: dict[str, Any] = { - "scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode] + "detection_callback": detection_callback, + "scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode], } if platform.system() == "Linux": # Only Linux supports multiple adapters @@ -117,16 +120,18 @@ class HaScanner(BaseHaScanner): over ethernet, usb over ethernet, etc. """ + scanner: bleak.BleakScanner + def __init__( self, hass: HomeAssistant, - scanner: bleak.BleakScanner, + mode: BluetoothScanningMode, adapter: str, address: str, ) -> None: """Init bluetooth discovery.""" self.hass = hass - self.scanner = scanner + self.mode = mode self.adapter = adapter self._start_stop_lock = asyncio.Lock() self._cancel_watchdog: CALLBACK_TYPE | None = None @@ -141,6 +146,13 @@ class HaScanner(BaseHaScanner): """Return a list of discovered devices.""" return self.scanner.discovered_devices + @hass_callback + def async_setup(self) -> None: + """Set up the scanner.""" + self.scanner = create_bleak_scanner( + self._async_detection_callback, self.mode, self.adapter + ) + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: """Get a device by address.""" if platform.system() == "Linux": @@ -218,8 +230,6 @@ class HaScanner(BaseHaScanner): async def async_start(self) -> None: """Start bluetooth scanner.""" - self.scanner.register_detection_callback(self._async_detection_callback) - async with self._start_stop_lock: await self._async_start() diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 7ee1a9840db..2e311d9d97e 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -2,7 +2,7 @@ import asyncio from datetime import timedelta import time -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import ANY, MagicMock, Mock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice @@ -114,6 +114,7 @@ async def test_setup_and_stop_passive(hass, mock_bleak_scanner_start, one_adapte "adapter": "hci0", "bluez": scanner.PASSIVE_SCANNER_ARGS, "scanning_mode": "passive", + "detection_callback": ANY, } @@ -161,6 +162,7 @@ async def test_setup_and_stop_old_bluez( assert init_kwargs == { "adapter": "hci0", "scanning_mode": "active", + "detection_callback": ANY, } diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 91e8ab50971..a4666352479 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -174,6 +174,10 @@ async def test_recovery_from_dbus_restart(hass, one_adapter): mock_discovered = [] class MockBleakScanner: + def __init__(self, detection_callback, *args, **kwargs): + nonlocal _callback + _callback = detection_callback + async def start(self, *args, **kwargs): """Mock Start.""" nonlocal called_start @@ -190,23 +194,15 @@ async def test_recovery_from_dbus_restart(hass, one_adapter): nonlocal mock_discovered return mock_discovered - def register_detection_callback(self, callback: AdvertisementDataCallback): - """Mock Register Detection Callback.""" - nonlocal _callback - _callback = callback - - scanner = MockBleakScanner() - with patch( "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", - return_value=scanner, + MockBleakScanner, ): await async_setup_with_one_adapter(hass) assert called_start == 1 start_time_monotonic = time.monotonic() - scanner = _get_manager() mock_discovered = [MagicMock()] # Ensure we don't restart the scanner if we don't need to From dd243e18e66dbe4df9ae1adb665f4200d7a6307c Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 3 Oct 2022 19:17:47 -0600 Subject: [PATCH 090/183] Remove repairs issue per PR review request (#79561) --- .../components/litterrobot/__init__.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index cf14239b22d..3d8f8487b33 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -6,8 +6,6 @@ from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, Robot from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN from .hub import LitterRobotHub @@ -36,21 +34,6 @@ def get_platforms_for_robots(robots: list[Robot]) -> set[Platform]: } -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Litter-Robot integration.""" - async_create_issue( - hass, - DOMAIN, - "migrated_attributes", - breaks_in_ha_version="2022.12.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="migrated_attributes", - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) From 572d15050f8b462242132d5c5f3c02c4ef1d77eb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 4 Oct 2022 00:45:31 +0200 Subject: [PATCH 091/183] Netatmo bump pyatmo to 7.1.0 (#79562) Bump pyatmo to 7.1.0 --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index b198c43bb39..4095762c666 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.0.1"], + "requirements": ["pyatmo==7.1.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index 5c02ca53814..114f9fc9b83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1436,7 +1436,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.0.1 +pyatmo==7.1.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22a10dbf4b7..535c6ec1fbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1024,7 +1024,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.0.1 +pyatmo==7.1.0 # homeassistant.components.apple_tv pyatv==0.10.3 From e13c6a526425ac2e391c8e346fca2970a0551f37 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 3 Oct 2022 20:58:53 -0400 Subject: [PATCH 092/183] Bump ZHA dependencies (#79565) Bump all ZHA dependencies --- homeassistant/components/zha/manifest.json | 14 +++++++------- requirements_all.txt | 14 +++++++------- requirements_test_all.txt | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 3f07d81ddb5..322f93e8373 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,15 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.33.1", + "bellows==0.34.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.80", - "zigpy-deconz==0.18.1", - "zigpy==0.50.3", - "zigpy-xbee==0.15.0", - "zigpy-zigate==0.9.2", - "zigpy-znp==0.8.2" + "zha-quirks==0.0.81", + "zigpy-deconz==0.19.0", + "zigpy==0.51.1", + "zigpy-xbee==0.16.0", + "zigpy-zigate==0.10.0", + "zigpy-znp==0.9.0" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 114f9fc9b83..1189fad6ba4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -401,7 +401,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.33.1 +bellows==0.34.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -2592,7 +2592,7 @@ zengge==0.2 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.80 +zha-quirks==0.0.81 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2601,19 +2601,19 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.18.1 +zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.15.0 +zigpy-xbee==0.16.0 # homeassistant.components.zha -zigpy-zigate==0.9.2 +zigpy-zigate==0.10.0 # homeassistant.components.zha -zigpy-znp==0.8.2 +zigpy-znp==0.9.0 # homeassistant.components.zha -zigpy==0.50.3 +zigpy==0.51.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 535c6ec1fbd..76975879277 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.33.1 +bellows==0.34.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -1793,22 +1793,22 @@ youless-api==0.16 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.80 +zha-quirks==0.0.81 # homeassistant.components.zha -zigpy-deconz==0.18.1 +zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.15.0 +zigpy-xbee==0.16.0 # homeassistant.components.zha -zigpy-zigate==0.9.2 +zigpy-zigate==0.10.0 # homeassistant.components.zha -zigpy-znp==0.8.2 +zigpy-znp==0.9.0 # homeassistant.components.zha -zigpy==0.50.3 +zigpy==0.51.1 # homeassistant.components.zwave_js zwave-js-server-python==0.42.0 From 7cfba93d5021ee6ada817d5c223e6a238efb4e04 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Oct 2022 21:19:03 -0400 Subject: [PATCH 093/183] Bumped version to 2022.10.0b5 --- 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 6bb835149b1..26deb059b99 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 = 10 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __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 f48fb8bed76..fdce778845b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0b4" +version = "2022.10.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 04a2a041c30f4a1a023a2350d929fe3f2f0c7231 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 4 Oct 2022 10:40:49 -0400 Subject: [PATCH 094/183] Bump zwave_js lib to 0.43.0 and fix multi-file firmware updates (#79342) --- homeassistant/components/zwave_js/api.py | 31 +-- .../components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/update.py | 77 +++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 47 +++-- tests/components/zwave_js/test_update.py | 182 +++--------------- 7 files changed, 111 insertions(+), 232 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 7ceca062ee4..4a5b233a2f0 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable import dataclasses from functools import partial, wraps -from typing import Any, Literal, cast +from typing import Any, Literal from aiohttp import web, web_exceptions, web_request import voluptuous as vol @@ -27,7 +27,7 @@ from zwave_js_server.exceptions import ( NotFoundError, SetValueFailed, ) -from zwave_js_server.firmware import begin_firmware_update +from zwave_js_server.firmware import update_firmware from zwave_js_server.model.controller import ( ControllerStatistics, InclusionGrant, @@ -36,8 +36,9 @@ from zwave_js_server.model.controller import ( ) from zwave_js_server.model.driver import Driver from zwave_js_server.model.firmware import ( - FirmwareUpdateFinished, + FirmwareUpdateData, FirmwareUpdateProgress, + FirmwareUpdateResult, ) from zwave_js_server.model.log_config import LogConfig from zwave_js_server.model.log_message import LogMessage @@ -1897,11 +1898,14 @@ async def websocket_is_node_firmware_update_in_progress( def _get_firmware_update_progress_dict( progress: FirmwareUpdateProgress, -) -> dict[str, int]: +) -> dict[str, int | float]: """Get a dictionary of firmware update progress.""" return { + "current_file": progress.current_file, + "total_files": progress.total_files, "sent_fragments": progress.sent_fragments, "total_fragments": progress.total_fragments, + "progress": progress.progress, } @@ -1943,14 +1947,16 @@ async def websocket_subscribe_firmware_update_status( @callback def forward_finished(event: dict) -> None: - finished: FirmwareUpdateFinished = event["firmware_update_finished"] + finished: FirmwareUpdateResult = event["firmware_update_finished"] connection.send_message( websocket_api.event_message( msg[ID], { "event": event["event"], "status": finished.status, + "success": finished.success, "wait_time": finished.wait_time, + "reinterview": finished.reinterview, }, ) ) @@ -2052,21 +2058,20 @@ class FirmwareUploadView(HomeAssistantView): if "file" not in data or not isinstance(data["file"], web_request.FileField): raise web_exceptions.HTTPBadRequest - target = None - if "target" in data: - target = int(cast(str, data["target"])) - uploaded_file: web_request.FileField = data["file"] try: - await begin_firmware_update( + await update_firmware( node.client.ws_server_url, node, - uploaded_file.filename, - await hass.async_add_executor_job(uploaded_file.file.read), + [ + FirmwareUpdateData( + uploaded_file.filename, + await hass.async_add_executor_job(uploaded_file.file.read), + ) + ], async_get_clientsession(hass), additional_user_agent_components=USER_AGENT, - target=target, ) except BaseZwaveJSServerError as err: raise web_exceptions.HTTPBadRequest(reason=str(err)) from err diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 8f0c93f6c3e..5b085ab0bb3 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.42.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 52c7e0d46e1..0c458d6e1a8 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from collections.abc import Callable from datetime import datetime, timedelta -from math import floor from typing import Any from awesomeversion import AwesomeVersion @@ -13,10 +12,9 @@ from zwave_js_server.const import NodeStatus from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver from zwave_js_server.model.firmware import ( - FirmwareUpdateFinished, FirmwareUpdateInfo, FirmwareUpdateProgress, - FirmwareUpdateStatus, + FirmwareUpdateResult, ) from zwave_js_server.model.node import Node as ZwaveNode @@ -91,9 +89,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._poll_unsub: Callable[[], None] | None = None self._progress_unsub: Callable[[], None] | None = None self._finished_unsub: Callable[[], None] | None = None - self._num_files_installed: int = 0 self._finished_event = asyncio.Event() - self._finished_status: FirmwareUpdateStatus | None = None + self._result: FirmwareUpdateResult | None = None # Entity class attributes self._attr_name = "Firmware" @@ -115,25 +112,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): progress: FirmwareUpdateProgress = event["firmware_update_progress"] if not self._latest_version_firmware: return - # We will assume that each file in the firmware update represents an equal - # percentage of the overall progress. This is likely not true because each file - # may be a different size, but it's the best we can do since we don't know the - # total number of fragments across all files. - self._attr_in_progress = floor( - 100 - * ( - self._num_files_installed - + (progress.sent_fragments / progress.total_fragments) - ) - / len(self._latest_version_firmware.files) - ) + self._attr_in_progress = int(progress.progress) self.async_write_ha_state() @callback def _update_finished(self, event: dict[str, Any]) -> None: """Update install progress on event.""" - finished: FirmwareUpdateFinished = event["firmware_update_finished"] - self._finished_status = finished.status + result: FirmwareUpdateResult = event["firmware_update_finished"] + self._result = result self._finished_event.set() @callback @@ -149,10 +135,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._finished_unsub() self._finished_unsub = None - self._finished_status = None + self._result = None self._finished_event.clear() - self._num_files_installed = 0 - self._attr_in_progress = 0 + self._attr_in_progress = False if write_state: self.async_write_ha_state() @@ -235,41 +220,23 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): "firmware update finished", self._update_finished ) - for file in firmware.files: - try: - await self.driver.controller.async_begin_ota_firmware_update( - self.node, file - ) - except BaseZwaveJSServerError as err: - self._unsub_firmware_events_and_reset_progress() - raise HomeAssistantError(err) from err - - # We need to block until we receive the `firmware update finished` event - await self._finished_event.wait() - # Clear the event so that a second firmware update blocks again - self._finished_event.clear() - assert self._finished_status is not None - - # If status is not OK, we should throw an error to let the user know - if self._finished_status not in ( - FirmwareUpdateStatus.OK_NO_RESTART, - FirmwareUpdateStatus.OK_RESTART_PENDING, - FirmwareUpdateStatus.OK_WAITING_FOR_ACTIVATION, - ): - status = self._finished_status - self._unsub_firmware_events_and_reset_progress() - raise HomeAssistantError(status.name.replace("_", " ").title()) - - # If we get here, the firmware installation was successful and we need to - # update progress accordingly - self._num_files_installed += 1 - self._attr_in_progress = floor( - 100 * self._num_files_installed / len(firmware.files) + try: + await self.driver.controller.async_firmware_update_ota( + self.node, firmware.files ) + except BaseZwaveJSServerError as err: + self._unsub_firmware_events_and_reset_progress() + raise HomeAssistantError(err) from err - # Clear the status so we can get a new one - self._finished_status = None - self.async_write_ha_state() + # We need to block until we receive the `firmware update finished` event + await self._finished_event.wait() + assert self._result is not None + + # If the update was not successful, we should throw an error to let the user know + if not self._result.success: + error_msg = self._result.status.name.replace("_", " ").title() + self._unsub_firmware_events_and_reset_progress() + raise HomeAssistantError(error_msg) # If we get here, all files were installed successfully self._attr_installed_version = self._attr_latest_version = firmware.version diff --git a/requirements_all.txt b/requirements_all.txt index 1189fad6ba4..070133aebfd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2619,7 +2619,7 @@ zigpy==0.51.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.42.0 +zwave-js-server-python==0.43.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76975879277..a47b3200225 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1811,7 +1811,7 @@ zigpy-znp==0.9.0 zigpy==0.51.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.42.0 +zwave-js-server-python==0.43.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.6 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index b55f4941a49..caea283e25c 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -28,6 +28,7 @@ from zwave_js_server.model.controller import ( ProvisioningEntry, QRProvisioningInformation, ) +from zwave_js_server.model.firmware import FirmwareUpdateData from zwave_js_server.model.node import Node from homeassistant.components.websocket_api import ERR_INVALID_FORMAT, ERR_NOT_FOUND @@ -2815,18 +2816,20 @@ async def test_firmware_upload_view( client = await hass_client() device = get_device(hass, multisensor_6) with patch( - "homeassistant.components.zwave_js.api.begin_firmware_update", + "homeassistant.components.zwave_js.api.update_firmware", ) as mock_cmd, patch.dict( "homeassistant.components.zwave_js.api.USER_AGENT", {"HomeAssistant": "0.0.0"}, ): resp = await client.post( f"/api/zwave_js/firmware/upload/{device.id}", - data={"file": firmware_file, "target": "15"}, + data={"file": firmware_file}, + ) + assert mock_cmd.call_args[0][1:3] == ( + multisensor_6, + [FirmwareUpdateData("file", bytes(10))], ) - assert mock_cmd.call_args[0][1:4] == (multisensor_6, "file", bytes(10)) assert mock_cmd.call_args[1] == { - "target": 15, "additional_user_agent_components": {"HomeAssistant": "0.0.0"}, } assert json.loads(await resp.text()) is None @@ -2839,7 +2842,7 @@ async def test_firmware_upload_view_failed_command( client = await hass_client() device = get_device(hass, multisensor_6) with patch( - "homeassistant.components.zwave_js.api.begin_firmware_update", + "homeassistant.components.zwave_js.api.update_firmware", side_effect=FailedCommand("test", "test"), ): resp = await client.post( @@ -3502,8 +3505,13 @@ async def test_subscribe_firmware_update_status( "source": "node", "event": "firmware update progress", "nodeId": multisensor_6.node_id, - "sentFragments": 1, - "totalFragments": 10, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 10, + "progress": 10.0, + }, }, ) multisensor_6.receive_event(event) @@ -3511,8 +3519,11 @@ async def test_subscribe_firmware_update_status( msg = await ws_client.receive_json() assert msg["event"] == { "event": "firmware update progress", + "current_file": 1, + "total_files": 1, "sent_fragments": 1, "total_fragments": 10, + "progress": 10.0, } event = Event( @@ -3521,8 +3532,12 @@ async def test_subscribe_firmware_update_status( "source": "node", "event": "firmware update finished", "nodeId": multisensor_6.node_id, - "status": 255, - "waitTime": 10, + "result": { + "status": 255, + "success": True, + "waitTime": 10, + "reInterview": False, + }, }, ) multisensor_6.receive_event(event) @@ -3531,7 +3546,9 @@ async def test_subscribe_firmware_update_status( assert msg["event"] == { "event": "firmware update finished", "status": 255, + "success": True, "wait_time": 10, + "reinterview": False, } @@ -3551,8 +3568,13 @@ async def test_subscribe_firmware_update_status_initial_value( "source": "node", "event": "firmware update progress", "nodeId": multisensor_6.node_id, - "sentFragments": 1, - "totalFragments": 10, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 10, + "progress": 10.0, + }, }, ) multisensor_6.receive_event(event) @@ -3574,8 +3596,11 @@ async def test_subscribe_firmware_update_status_initial_value( msg = await ws_client.receive_json() assert msg["event"] == { "event": "firmware update progress", + "current_file": 1, + "total_files": 1, "sent_fragments": 1, "total_fragments": 10, + "progress": 10.0, } diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index b2517c3dd34..4c00c1c9a3a 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -324,7 +324,7 @@ async def test_update_entity_progress( assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = None + client.async_send_command.return_value = {"success": False} # Test successful install call without a version install_task = hass.async_create_task( @@ -352,8 +352,13 @@ async def test_update_entity_progress( "source": "node", "event": "firmware update progress", "nodeId": node.node_id, - "sentFragments": 1, - "totalFragments": 20, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, }, ) node.receive_event(event) @@ -370,7 +375,11 @@ async def test_update_entity_progress( "source": "node", "event": "firmware update finished", "nodeId": node.node_id, - "status": FirmwareUpdateStatus.OK_NO_RESTART, + "result": { + "status": FirmwareUpdateStatus.OK_NO_RESTART, + "success": True, + "reInterview": False, + }, }, ) @@ -381,142 +390,7 @@ async def test_update_entity_progress( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 0 - assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4" - assert attrs[ATTR_LATEST_VERSION] == "11.2.4" - assert state.state == STATE_OFF - - await install_task - - -async def test_update_entity_progress_multiple( - hass, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, -): - """Test update entity progress with multiple files.""" - node = climate_radio_thermostat_ct100_plus_different_endpoints - client.async_send_command.return_value = FIRMWARE_UPDATE_MULTIPLE_FILES - - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) - await hass.async_block_till_done() - - state = hass.states.get(UPDATE_ENTITY) - assert state - assert state.state == STATE_ON - attrs = state.attributes - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" - assert attrs[ATTR_LATEST_VERSION] == "11.2.4" - - client.async_send_command.reset_mock() - client.async_send_command.return_value = None - - # Test successful install call without a version - install_task = hass.async_create_task( - hass.services.async_call( - UPDATE_DOMAIN, - SERVICE_INSTALL, - { - ATTR_ENTITY_ID: UPDATE_ENTITY, - }, - blocking=True, - ) - ) - - # Sleep so that task starts - await asyncio.sleep(0.1) - - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] is True - - node.receive_event( - Event( - type="firmware update progress", - data={ - "source": "node", - "event": "firmware update progress", - "nodeId": node.node_id, - "sentFragments": 1, - "totalFragments": 20, - }, - ) - ) - - # Block so HA can do its thing - await asyncio.sleep(0) - - # Validate that the progress is updated (two files means progress is 50% of 5) - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 2 - - node.receive_event( - Event( - type="firmware update finished", - data={ - "source": "node", - "event": "firmware update finished", - "nodeId": node.node_id, - "status": FirmwareUpdateStatus.OK_NO_RESTART, - }, - ) - ) - - # Block so HA can do its thing - await asyncio.sleep(0) - - # One file done, progress should be 50% - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 50 - - node.receive_event( - Event( - type="firmware update progress", - data={ - "source": "node", - "event": "firmware update progress", - "nodeId": node.node_id, - "sentFragments": 1, - "totalFragments": 20, - }, - ) - ) - - # Block so HA can do its thing - await asyncio.sleep(0) - - # Validate that the progress is updated (50% + 50% of 5) - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 52 - - node.receive_event( - Event( - type="firmware update finished", - data={ - "source": "node", - "event": "firmware update finished", - "nodeId": node.node_id, - "status": FirmwareUpdateStatus.OK_NO_RESTART, - }, - ) - ) - - # Block so HA can do its thing - await asyncio.sleep(0) - - # Validate that progress is reset and entity reflects new version - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 0 + assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_OFF @@ -546,10 +420,11 @@ async def test_update_entity_install_failed( assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = None + client.async_send_command.return_value = {"success": False} - async def call_install(): - await hass.services.async_call( + # Test install call - we expect it to finish fail + install_task = hass.async_create_task( + hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, { @@ -557,9 +432,7 @@ async def test_update_entity_install_failed( }, blocking=True, ) - - # Test install call - we expect it to raise - install_task = hass.async_create_task(call_install()) + ) # Sleep so that task starts await asyncio.sleep(0.1) @@ -570,8 +443,13 @@ async def test_update_entity_install_failed( "source": "node", "event": "firmware update progress", "nodeId": node.node_id, - "sentFragments": 1, - "totalFragments": 20, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, }, ) node.receive_event(event) @@ -588,7 +466,11 @@ async def test_update_entity_install_failed( "source": "node", "event": "firmware update finished", "nodeId": node.node_id, - "status": FirmwareUpdateStatus.ERROR_TIMEOUT, + "result": { + "status": FirmwareUpdateStatus.ERROR_TIMEOUT, + "success": False, + "reInterview": False, + }, }, ) @@ -599,7 +481,7 @@ async def test_update_entity_install_failed( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 0 + assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_INSTALLED_VERSION] == "10.7" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_ON From 5957c6a185bb39cc9ed55768f0f4f17b40538680 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Mon, 3 Oct 2022 11:10:25 +0200 Subject: [PATCH 095/183] Address late review of ViCare (#79458) Runn blocking I/O of button entity creation in async_add_executor_job --- homeassistant/components/vicare/button.py | 41 +++++++++++++++-------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index 6f94c7102c9..95be680f957 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -1,4 +1,4 @@ -"""Viessmann ViCare sensor device.""" +"""Viessmann ViCare button device.""" from __future__ import annotations from contextlib import suppress @@ -30,7 +30,7 @@ BUTTON_DHW_ACTIVATE_ONETIME_CHARGE = "activate_onetimecharge" class ViCareButtonEntityDescription( ButtonEntityDescription, ViCareRequiredKeysMixinWithSet ): - """Describes ViCare button sensor entity.""" + """Describes ViCare button entity.""" BUTTON_DESCRIPTIONS: tuple[ViCareButtonEntityDescription, ...] = ( @@ -45,28 +45,41 @@ BUTTON_DESCRIPTIONS: tuple[ViCareButtonEntityDescription, ...] = ( ) +def _build_entity(name, vicare_api, device_config, description): + """Create a ViCare button entity.""" + _LOGGER.debug("Found device %s", name) + try: + description.value_getter(vicare_api) + _LOGGER.debug("Found entity %s", name) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("Feature not supported %s", name) + return None + except AttributeError: + _LOGGER.debug("Attribute Error %s", name) + return None + + return ViCareButton( + name, + vicare_api, + device_config, + description, + ) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Create the ViCare binary sensor devices.""" + """Create the ViCare button entities.""" name = VICARE_NAME api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API] entities = [] for description in BUTTON_DESCRIPTIONS: - try: - description.value_getter(api) - _LOGGER.debug("Found entity %s", description.name) - except PyViCareNotSupportedFeatureError: - _LOGGER.info("Feature not supported %s", description.name) - continue - except AttributeError: - _LOGGER.debug("Attribute Error %s", name) - continue - entity = ViCareButton( + entity = await hass.async_add_executor_job( + _build_entity, f"{name} {description.name}", api, hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], @@ -86,7 +99,7 @@ class ViCareButton(ButtonEntity): def __init__( self, name, api, device_config, description: ViCareButtonEntityDescription ): - """Initialize the sensor.""" + """Initialize the button.""" self.entity_description = description self._device_config = device_config self._api = api From 39be6ecd00f436da87265f76a437fb219ca6e3d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Oct 2022 16:44:54 -1000 Subject: [PATCH 096/183] Bump dbus-fast to 1.23.0 (#79570) --- 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 1cb01a7da63..3b6f5977157 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.22.0" + "dbus-fast==1.23.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index aeb65b379ba..e3dd8b504a5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.22.0 +dbus-fast==1.23.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 070133aebfd..c0ed200b1d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.22.0 +dbus-fast==1.23.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a47b3200225..5a333ff8299 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.22.0 +dbus-fast==1.23.0 # homeassistant.components.debugpy debugpy==1.6.3 From 310dbe90a15e96f9971331db3765544d3f9fc906 Mon Sep 17 00:00:00 2001 From: kpine Date: Tue, 4 Oct 2022 02:54:13 -0700 Subject: [PATCH 097/183] Set zwave_js climate entity target temp attributes based on current mode (#79575) * Report temperature correctly * DRY * Add test assertions * Don't catch TypeError (revert) --- homeassistant/components/zwave_js/climate.py | 62 +++++++++++++------- tests/components/zwave_js/test_climate.py | 5 ++ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 07119d365e0..e2bd69a1436 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -201,13 +201,25 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if self._fan_mode: self._attr_supported_features |= ClimateEntityFeature.FAN_MODE - def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue: - """Optionally return a ZwaveValue for a setpoint.""" + def _setpoint_value_or_raise( + self, setpoint_type: ThermostatSetpointType + ) -> ZwaveValue: + """Return a ZwaveValue for a setpoint or raise if not available.""" if (val := self._setpoint_values[setpoint_type]) is None: raise ValueError("Value requested is not available") return val + def _setpoint_temperature( + self, setpoint_type: ThermostatSetpointType + ) -> float | None: + """Optionally return the temperature value of a setpoint.""" + try: + temp = self._setpoint_value_or_raise(setpoint_type) + except (IndexError, ValueError): + return None + return get_value_of_zwave_value(temp) + def _set_modes_and_presets(self) -> None: """Convert Z-Wave Thermostat modes into Home Assistant modes and presets.""" all_modes: dict[HVACMode, int | None] = {} @@ -290,36 +302,44 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - if self._current_mode and self._current_mode.value is None: + if ( + self._current_mode and self._current_mode.value is None + ) or not self._current_mode_setpoint_enums: # guard missing value return None - try: - temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) - except (IndexError, ValueError): + if len(self._current_mode_setpoint_enums) > 1: + # current mode has a temperature range return None - return get_value_of_zwave_value(temp) + + return self._setpoint_temperature(self._current_mode_setpoint_enums[0]) @property def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" - if self._current_mode and self._current_mode.value is None: + if ( + self._current_mode and self._current_mode.value is None + ) or not self._current_mode_setpoint_enums: # guard missing value return None - try: - temp = self._setpoint_value(self._current_mode_setpoint_enums[1]) - except (IndexError, ValueError): + if len(self._current_mode_setpoint_enums) < 2: + # current mode has a single temperature return None - return get_value_of_zwave_value(temp) + + return self._setpoint_temperature(self._current_mode_setpoint_enums[1]) @property def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" - if self._current_mode and self._current_mode.value is None: + if ( + self._current_mode and self._current_mode.value is None + ) or not self._current_mode_setpoint_enums: # guard missing value return None - if len(self._current_mode_setpoint_enums) > 1: - return self.target_temperature - return None + if len(self._current_mode_setpoint_enums) < 2: + # current mode has a single temperature + return None + + return self._setpoint_temperature(self._current_mode_setpoint_enums[0]) @property def preset_mode(self) -> str | None: @@ -380,7 +400,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): min_temp = DEFAULT_MIN_TEMP base_unit = TEMP_CELSIUS try: - temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) + temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) if temp.metadata.min: min_temp = temp.metadata.min base_unit = self.temperature_unit @@ -396,7 +416,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): max_temp = DEFAULT_MAX_TEMP base_unit = TEMP_CELSIUS try: - temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) + temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) if temp.metadata.max: max_temp = temp.metadata.max base_unit = self.temperature_unit @@ -431,17 +451,17 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if hvac_mode is not None: await self.async_set_hvac_mode(hvac_mode) if len(self._current_mode_setpoint_enums) == 1: - setpoint: ZwaveValue = self._setpoint_value( + setpoint: ZwaveValue = self._setpoint_value_or_raise( self._current_mode_setpoint_enums[0] ) target_temp: float | None = kwargs.get(ATTR_TEMPERATURE) if target_temp is not None: await self.info.node.async_set_value(setpoint, target_temp) elif len(self._current_mode_setpoint_enums) == 2: - setpoint_low: ZwaveValue = self._setpoint_value( + setpoint_low: ZwaveValue = self._setpoint_value_or_raise( self._current_mode_setpoint_enums[0] ) - setpoint_high: ZwaveValue = self._setpoint_value( + setpoint_high: ZwaveValue = self._setpoint_value_or_raise( self._current_mode_setpoint_enums[1] ) target_temp_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW) diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 4b4519c07b9..93d1849f451 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -58,6 +58,8 @@ async def test_thermostat_v2( assert state.attributes[ATTR_CURRENT_HUMIDITY] == 30 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.2 assert state.attributes[ATTR_TEMPERATURE] == 22.2 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE assert state.attributes[ATTR_FAN_MODE] == "Auto low" assert state.attributes[ATTR_FAN_STATE] == "Idle / off" @@ -152,6 +154,8 @@ async def test_thermostat_v2( state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) assert state.state == HVACMode.COOL assert state.attributes[ATTR_TEMPERATURE] == 22.8 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None # Test heat_cool mode update from value updated event event = Event( @@ -175,6 +179,7 @@ async def test_thermostat_v2( state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) assert state.state == HVACMode.HEAT_COOL + assert state.attributes[ATTR_TEMPERATURE] is None assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 22.8 assert state.attributes[ATTR_TARGET_TEMP_LOW] == 22.2 From 9ca179ddcb124b4ba62ff2fed58da5a6268b4a1a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 Oct 2022 15:24:55 +0200 Subject: [PATCH 098/183] Collect all brands (#79579) --- homeassistant/brands/amazon.json | 5 + homeassistant/brands/apple.json | 7 +- homeassistant/brands/aruba.json | 5 + homeassistant/brands/asterisk.json | 5 + homeassistant/brands/august.json | 5 + homeassistant/brands/cisco.json | 5 + homeassistant/brands/clicksend.json | 5 + homeassistant/brands/devolo.json | 5 + homeassistant/brands/dlna.json | 5 + homeassistant/brands/elgato.json | 5 + homeassistant/brands/emoncms.json | 5 + homeassistant/brands/epson.json | 5 + homeassistant/brands/eq3.json | 5 + homeassistant/brands/ffmpeg.json | 5 + homeassistant/brands/geonet.json | 5 + homeassistant/brands/globalcache.json | 5 + homeassistant/brands/hikvision.json | 5 + homeassistant/brands/homematic.json | 5 + homeassistant/brands/honeywell.json | 5 + homeassistant/brands/ibm.json | 5 + homeassistant/brands/lg.json | 5 + homeassistant/brands/logitech.json | 5 + homeassistant/brands/lutron.json | 5 + homeassistant/brands/melnor.json | 5 + homeassistant/brands/microsoft.json | 16 + homeassistant/brands/mqtt.json | 12 + homeassistant/brands/netgear.json | 5 + homeassistant/brands/openwrt.json | 5 + homeassistant/brands/panasonic.json | 5 + homeassistant/brands/philips.json | 5 + homeassistant/brands/qnap.json | 5 + homeassistant/brands/raspberry.json | 5 + homeassistant/brands/russound.json | 5 + homeassistant/brands/samsung.json | 5 + homeassistant/brands/solaredge.json | 5 + homeassistant/brands/sony.json | 5 + homeassistant/brands/synology.json | 5 + homeassistant/brands/telegram.json | 5 + homeassistant/brands/telldus.json | 5 + homeassistant/brands/tesla.json | 5 + homeassistant/brands/trafikverket.json | 9 + homeassistant/brands/twilio.json | 5 + homeassistant/brands/vlc.json | 5 + homeassistant/brands/xiaomi.json | 11 + homeassistant/brands/yale.json | 5 + homeassistant/brands/yandex.json | 5 + homeassistant/brands/yeelight.json | 5 + homeassistant/generated/integrations.json | 1366 ++++++++++++--------- 48 files changed, 1065 insertions(+), 566 deletions(-) create mode 100644 homeassistant/brands/amazon.json create mode 100644 homeassistant/brands/aruba.json create mode 100644 homeassistant/brands/asterisk.json create mode 100644 homeassistant/brands/august.json create mode 100644 homeassistant/brands/cisco.json create mode 100644 homeassistant/brands/clicksend.json create mode 100644 homeassistant/brands/devolo.json create mode 100644 homeassistant/brands/dlna.json create mode 100644 homeassistant/brands/elgato.json create mode 100644 homeassistant/brands/emoncms.json create mode 100644 homeassistant/brands/epson.json create mode 100644 homeassistant/brands/eq3.json create mode 100644 homeassistant/brands/ffmpeg.json create mode 100644 homeassistant/brands/geonet.json create mode 100644 homeassistant/brands/globalcache.json create mode 100644 homeassistant/brands/hikvision.json create mode 100644 homeassistant/brands/homematic.json create mode 100644 homeassistant/brands/honeywell.json create mode 100644 homeassistant/brands/ibm.json create mode 100644 homeassistant/brands/lg.json create mode 100644 homeassistant/brands/logitech.json create mode 100644 homeassistant/brands/lutron.json create mode 100644 homeassistant/brands/melnor.json create mode 100644 homeassistant/brands/microsoft.json create mode 100644 homeassistant/brands/mqtt.json create mode 100644 homeassistant/brands/netgear.json create mode 100644 homeassistant/brands/openwrt.json create mode 100644 homeassistant/brands/panasonic.json create mode 100644 homeassistant/brands/philips.json create mode 100644 homeassistant/brands/qnap.json create mode 100644 homeassistant/brands/raspberry.json create mode 100644 homeassistant/brands/russound.json create mode 100644 homeassistant/brands/samsung.json create mode 100644 homeassistant/brands/solaredge.json create mode 100644 homeassistant/brands/sony.json create mode 100644 homeassistant/brands/synology.json create mode 100644 homeassistant/brands/telegram.json create mode 100644 homeassistant/brands/telldus.json create mode 100644 homeassistant/brands/tesla.json create mode 100644 homeassistant/brands/trafikverket.json create mode 100644 homeassistant/brands/twilio.json create mode 100644 homeassistant/brands/vlc.json create mode 100644 homeassistant/brands/xiaomi.json create mode 100644 homeassistant/brands/yale.json create mode 100644 homeassistant/brands/yandex.json create mode 100644 homeassistant/brands/yeelight.json diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json new file mode 100644 index 00000000000..e31bb410457 --- /dev/null +++ b/homeassistant/brands/amazon.json @@ -0,0 +1,5 @@ +{ + "domain": "amazon", + "name": "Amazon", + "integrations": ["alexa", "amazon_polly", "aws", "route53"] +} diff --git a/homeassistant/brands/apple.json b/homeassistant/brands/apple.json index 1a782b50900..00f646e435e 100644 --- a/homeassistant/brands/apple.json +++ b/homeassistant/brands/apple.json @@ -2,10 +2,11 @@ "domain": "apple", "name": "Apple", "integrations": [ - "icloud", - "ibeacon", "apple_tv", + "homekit_controller", "homekit", - "homekit_controller" + "ibeacon", + "icloud", + "itunes" ] } diff --git a/homeassistant/brands/aruba.json b/homeassistant/brands/aruba.json new file mode 100644 index 00000000000..512192813e4 --- /dev/null +++ b/homeassistant/brands/aruba.json @@ -0,0 +1,5 @@ +{ + "domain": "aruba", + "name": "Aruba", + "integrations": ["aruba", "cppm_tracker"] +} diff --git a/homeassistant/brands/asterisk.json b/homeassistant/brands/asterisk.json new file mode 100644 index 00000000000..1df3e660afe --- /dev/null +++ b/homeassistant/brands/asterisk.json @@ -0,0 +1,5 @@ +{ + "domain": "asterisk", + "name": "Asterisk", + "integrations": ["asterisk_cdr", "asterisk_mbox"] +} diff --git a/homeassistant/brands/august.json b/homeassistant/brands/august.json new file mode 100644 index 00000000000..ce2f18dc759 --- /dev/null +++ b/homeassistant/brands/august.json @@ -0,0 +1,5 @@ +{ + "domain": "august", + "name": "August Home", + "integrations": ["august", "yalexs_ble"] +} diff --git a/homeassistant/brands/cisco.json b/homeassistant/brands/cisco.json new file mode 100644 index 00000000000..a1885b1af5e --- /dev/null +++ b/homeassistant/brands/cisco.json @@ -0,0 +1,5 @@ +{ + "domain": "cisco", + "name": "Cisco", + "integrations": ["cisco_ios", "cisco_mobility_express", "cisco_webex_teams"] +} diff --git a/homeassistant/brands/clicksend.json b/homeassistant/brands/clicksend.json new file mode 100644 index 00000000000..07de60a99e3 --- /dev/null +++ b/homeassistant/brands/clicksend.json @@ -0,0 +1,5 @@ +{ + "domain": "clicksend", + "name": "ClickSend", + "integrations": ["clicksend", "clicksend_tts"] +} diff --git a/homeassistant/brands/devolo.json b/homeassistant/brands/devolo.json new file mode 100644 index 00000000000..86dc7a3b100 --- /dev/null +++ b/homeassistant/brands/devolo.json @@ -0,0 +1,5 @@ +{ + "domain": "devolo", + "name": "devolo", + "integrations": ["devolo_home_control", "devolo_home_network"] +} diff --git a/homeassistant/brands/dlna.json b/homeassistant/brands/dlna.json new file mode 100644 index 00000000000..f6a648d6895 --- /dev/null +++ b/homeassistant/brands/dlna.json @@ -0,0 +1,5 @@ +{ + "domain": "dlna", + "name": "DLNA", + "integrations": ["dlna_dmr", "dlna_dms"] +} diff --git a/homeassistant/brands/elgato.json b/homeassistant/brands/elgato.json new file mode 100644 index 00000000000..3ca7e07c1bb --- /dev/null +++ b/homeassistant/brands/elgato.json @@ -0,0 +1,5 @@ +{ + "domain": "elgato", + "name": "Elgato", + "integrations": ["avea", "elgato"] +} diff --git a/homeassistant/brands/emoncms.json b/homeassistant/brands/emoncms.json new file mode 100644 index 00000000000..866c7ff18f3 --- /dev/null +++ b/homeassistant/brands/emoncms.json @@ -0,0 +1,5 @@ +{ + "domain": "emoncms", + "name": "emoncms", + "integrations": ["emoncms", "emoncms_history"] +} diff --git a/homeassistant/brands/epson.json b/homeassistant/brands/epson.json new file mode 100644 index 00000000000..80d5db942a2 --- /dev/null +++ b/homeassistant/brands/epson.json @@ -0,0 +1,5 @@ +{ + "domain": "epson", + "name": "Epson", + "integrations": ["epson", "epsonworkforce"] +} diff --git a/homeassistant/brands/eq3.json b/homeassistant/brands/eq3.json new file mode 100644 index 00000000000..4052afac277 --- /dev/null +++ b/homeassistant/brands/eq3.json @@ -0,0 +1,5 @@ +{ + "domain": "eq3", + "name": "eQ-3", + "integrations": ["eq3btsmart", "maxcube"] +} diff --git a/homeassistant/brands/ffmpeg.json b/homeassistant/brands/ffmpeg.json new file mode 100644 index 00000000000..2ec1de4ec03 --- /dev/null +++ b/homeassistant/brands/ffmpeg.json @@ -0,0 +1,5 @@ +{ + "domain": "ffmpeg", + "name": "FFmpeg", + "integrations": ["ffmpeg", "ffmpeg_motion", "ffmpeg_noise"] +} diff --git a/homeassistant/brands/geonet.json b/homeassistant/brands/geonet.json new file mode 100644 index 00000000000..4f09d607f80 --- /dev/null +++ b/homeassistant/brands/geonet.json @@ -0,0 +1,5 @@ +{ + "domain": "geonet", + "name": "GeoNet", + "integrations": ["geonetnz_quakes", "geonetnz_volcano"] +} diff --git a/homeassistant/brands/globalcache.json b/homeassistant/brands/globalcache.json new file mode 100644 index 00000000000..0cba9d65d0d --- /dev/null +++ b/homeassistant/brands/globalcache.json @@ -0,0 +1,5 @@ +{ + "domain": "globalcache", + "name": "Global Caché", + "integrations": ["gc100", "itach"] +} diff --git a/homeassistant/brands/hikvision.json b/homeassistant/brands/hikvision.json new file mode 100644 index 00000000000..b09770bccc5 --- /dev/null +++ b/homeassistant/brands/hikvision.json @@ -0,0 +1,5 @@ +{ + "domain": "hikvision", + "name": "Hikvision", + "integrations": ["hikvision", "hikvisioncam"] +} diff --git a/homeassistant/brands/homematic.json b/homeassistant/brands/homematic.json new file mode 100644 index 00000000000..e7f29c19d67 --- /dev/null +++ b/homeassistant/brands/homematic.json @@ -0,0 +1,5 @@ +{ + "domain": "homematic", + "name": "Homematic", + "integrations": ["homematic", "homematicip_cloud"] +} diff --git a/homeassistant/brands/honeywell.json b/homeassistant/brands/honeywell.json new file mode 100644 index 00000000000..37cd6d8ce73 --- /dev/null +++ b/homeassistant/brands/honeywell.json @@ -0,0 +1,5 @@ +{ + "domain": "honeywell", + "name": "Honeywell", + "integrations": ["lyric", "evohome", "honeywell"] +} diff --git a/homeassistant/brands/ibm.json b/homeassistant/brands/ibm.json new file mode 100644 index 00000000000..42367e899e7 --- /dev/null +++ b/homeassistant/brands/ibm.json @@ -0,0 +1,5 @@ +{ + "domain": "ibm", + "name": "IBM", + "integrations": ["watson_iot", "watson_tts"] +} diff --git a/homeassistant/brands/lg.json b/homeassistant/brands/lg.json new file mode 100644 index 00000000000..350db80b5f3 --- /dev/null +++ b/homeassistant/brands/lg.json @@ -0,0 +1,5 @@ +{ + "domain": "lg", + "name": "LG", + "integrations": ["lg_netcast", "lg_soundbar", "webostv"] +} diff --git a/homeassistant/brands/logitech.json b/homeassistant/brands/logitech.json new file mode 100644 index 00000000000..d4a0dd1bb87 --- /dev/null +++ b/homeassistant/brands/logitech.json @@ -0,0 +1,5 @@ +{ + "domain": "logitech", + "name": "Logitech", + "integrations": ["harmony", "ue_smart_radio", "squeezebox"] +} diff --git a/homeassistant/brands/lutron.json b/homeassistant/brands/lutron.json new file mode 100644 index 00000000000..b891065d819 --- /dev/null +++ b/homeassistant/brands/lutron.json @@ -0,0 +1,5 @@ +{ + "domain": "lutron", + "name": "Lutron", + "integrations": ["lutron", "lutron_caseta", "homeworks"] +} diff --git a/homeassistant/brands/melnor.json b/homeassistant/brands/melnor.json new file mode 100644 index 00000000000..c04db5c4e7c --- /dev/null +++ b/homeassistant/brands/melnor.json @@ -0,0 +1,5 @@ +{ + "domain": "melnor", + "name": "Melnor", + "integrations": ["melnor", "raincloud"] +} diff --git a/homeassistant/brands/microsoft.json b/homeassistant/brands/microsoft.json new file mode 100644 index 00000000000..d28932082a6 --- /dev/null +++ b/homeassistant/brands/microsoft.json @@ -0,0 +1,16 @@ +{ + "domain": "microsoft", + "name": "Microsoft", + "integrations": [ + "azure_devops", + "azure_event_hub", + "azure_service_bus", + "microsoft_face_detect", + "microsoft_face_identify", + "microsoft_face", + "microsoft", + "msteams", + "xbox", + "xbox_live" + ] +} diff --git a/homeassistant/brands/mqtt.json b/homeassistant/brands/mqtt.json new file mode 100644 index 00000000000..c1d58521a7c --- /dev/null +++ b/homeassistant/brands/mqtt.json @@ -0,0 +1,12 @@ +{ + "domain": "mqtt", + "name": "MQTT", + "integrations": [ + "manual_mqtt", + "mqtt", + "mqtt_eventstream", + "mqtt_json", + "mqtt_room", + "mqtt_statestream" + ] +} diff --git a/homeassistant/brands/netgear.json b/homeassistant/brands/netgear.json new file mode 100644 index 00000000000..9a6b6e51da0 --- /dev/null +++ b/homeassistant/brands/netgear.json @@ -0,0 +1,5 @@ +{ + "domain": "netgear", + "name": "NETGEAR", + "integrations": ["netgear", "netgear_lte"] +} diff --git a/homeassistant/brands/openwrt.json b/homeassistant/brands/openwrt.json new file mode 100644 index 00000000000..ff9cd4ca250 --- /dev/null +++ b/homeassistant/brands/openwrt.json @@ -0,0 +1,5 @@ +{ + "domain": "openwrt", + "name": "OpenWrt", + "integrations": ["luci", "ubus"] +} diff --git a/homeassistant/brands/panasonic.json b/homeassistant/brands/panasonic.json new file mode 100644 index 00000000000..2d8f29a3968 --- /dev/null +++ b/homeassistant/brands/panasonic.json @@ -0,0 +1,5 @@ +{ + "domain": "panasonic", + "name": "Panasonic", + "integrations": ["panasonic_bluray", "panasonic_viera"] +} diff --git a/homeassistant/brands/philips.json b/homeassistant/brands/philips.json new file mode 100644 index 00000000000..bfd290eb945 --- /dev/null +++ b/homeassistant/brands/philips.json @@ -0,0 +1,5 @@ +{ + "domain": "philips", + "name": "Philips", + "integrations": ["dynalite", "hue", "philips_js"] +} diff --git a/homeassistant/brands/qnap.json b/homeassistant/brands/qnap.json new file mode 100644 index 00000000000..6464a0ec877 --- /dev/null +++ b/homeassistant/brands/qnap.json @@ -0,0 +1,5 @@ +{ + "domain": "qnap", + "name": "QNAP", + "integrations": ["qnap", "qnap_qsw"] +} diff --git a/homeassistant/brands/raspberry.json b/homeassistant/brands/raspberry.json new file mode 100644 index 00000000000..a0ec6f12699 --- /dev/null +++ b/homeassistant/brands/raspberry.json @@ -0,0 +1,5 @@ +{ + "domain": "raspberry_pi", + "name": "Raspberry Pi", + "integrations": ["rpi_camera", "rpi_power", "remote_rpi_gpio"] +} diff --git a/homeassistant/brands/russound.json b/homeassistant/brands/russound.json new file mode 100644 index 00000000000..70b3de109ca --- /dev/null +++ b/homeassistant/brands/russound.json @@ -0,0 +1,5 @@ +{ + "domain": "russound", + "name": "Russound", + "integrations": ["russound_rio", "russound_rnet"] +} diff --git a/homeassistant/brands/samsung.json b/homeassistant/brands/samsung.json new file mode 100644 index 00000000000..1d5f2522e9e --- /dev/null +++ b/homeassistant/brands/samsung.json @@ -0,0 +1,5 @@ +{ + "domain": "samsung", + "name": "Samsung", + "integrations": ["familyhub", "samsungtv", "syncthru"] +} diff --git a/homeassistant/brands/solaredge.json b/homeassistant/brands/solaredge.json new file mode 100644 index 00000000000..90190f9c786 --- /dev/null +++ b/homeassistant/brands/solaredge.json @@ -0,0 +1,5 @@ +{ + "domain": "solaredge", + "name": "SolarEdge", + "integrations": ["solaredge", "solaredge_local"] +} diff --git a/homeassistant/brands/sony.json b/homeassistant/brands/sony.json new file mode 100644 index 00000000000..e35d5f4723c --- /dev/null +++ b/homeassistant/brands/sony.json @@ -0,0 +1,5 @@ +{ + "domain": "sony", + "name": "Sony", + "integrations": ["braviatv", "ps4", "sony_projector", "songpal"] +} diff --git a/homeassistant/brands/synology.json b/homeassistant/brands/synology.json new file mode 100644 index 00000000000..0387fabffaf --- /dev/null +++ b/homeassistant/brands/synology.json @@ -0,0 +1,5 @@ +{ + "domain": "synology", + "name": "Synology", + "integrations": ["synology_chat", "synology_dsm", "synology_srm"] +} diff --git a/homeassistant/brands/telegram.json b/homeassistant/brands/telegram.json new file mode 100644 index 00000000000..8cb5e202190 --- /dev/null +++ b/homeassistant/brands/telegram.json @@ -0,0 +1,5 @@ +{ + "domain": "telegram", + "name": "Telegram", + "integrations": ["telegram", "telegram_bot"] +} diff --git a/homeassistant/brands/telldus.json b/homeassistant/brands/telldus.json new file mode 100644 index 00000000000..c280832f68e --- /dev/null +++ b/homeassistant/brands/telldus.json @@ -0,0 +1,5 @@ +{ + "domain": "telldus", + "name": "Telldus", + "integrations": ["tellduslive", "tellstick"] +} diff --git a/homeassistant/brands/tesla.json b/homeassistant/brands/tesla.json new file mode 100644 index 00000000000..aeec7982579 --- /dev/null +++ b/homeassistant/brands/tesla.json @@ -0,0 +1,5 @@ +{ + "domain": "tesla", + "name": "Tesla", + "integrations": ["powerwall", "tesla_wall_connector"] +} diff --git a/homeassistant/brands/trafikverket.json b/homeassistant/brands/trafikverket.json new file mode 100644 index 00000000000..df444cbeb60 --- /dev/null +++ b/homeassistant/brands/trafikverket.json @@ -0,0 +1,9 @@ +{ + "domain": "trafikverket", + "name": "Trafikverket", + "integrations": [ + "trafikverket_ferry", + "trafikverket_train", + "trafikverket_weatherstation" + ] +} diff --git a/homeassistant/brands/twilio.json b/homeassistant/brands/twilio.json new file mode 100644 index 00000000000..7ae9162059e --- /dev/null +++ b/homeassistant/brands/twilio.json @@ -0,0 +1,5 @@ +{ + "domain": "twilio", + "name": "Twilio", + "integrations": ["twilio", "twilio_call", "twilio_sms"] +} diff --git a/homeassistant/brands/vlc.json b/homeassistant/brands/vlc.json new file mode 100644 index 00000000000..66c004470d6 --- /dev/null +++ b/homeassistant/brands/vlc.json @@ -0,0 +1,5 @@ +{ + "domain": "vlc", + "name": "VideoLAN", + "integrations": ["vlc", "vlc_telnet"] +} diff --git a/homeassistant/brands/xiaomi.json b/homeassistant/brands/xiaomi.json new file mode 100644 index 00000000000..ebdc99d8c38 --- /dev/null +++ b/homeassistant/brands/xiaomi.json @@ -0,0 +1,11 @@ +{ + "domain": "xiaomi", + "name": "Xiaomi", + "integrations": [ + "xiaomi_aqara", + "xiaomi_ble", + "xiaomi_miio", + "xiaomi_tv", + "xiaomi" + ] +} diff --git a/homeassistant/brands/yale.json b/homeassistant/brands/yale.json new file mode 100644 index 00000000000..87c119fdd40 --- /dev/null +++ b/homeassistant/brands/yale.json @@ -0,0 +1,5 @@ +{ + "domain": "yale", + "name": "Yale", + "integrations": ["august", "yale_smart_alarm", "yalexs_ble"] +} diff --git a/homeassistant/brands/yandex.json b/homeassistant/brands/yandex.json new file mode 100644 index 00000000000..c4a55be8b5e --- /dev/null +++ b/homeassistant/brands/yandex.json @@ -0,0 +1,5 @@ +{ + "domain": "yandex", + "name": "Yandex", + "integrations": ["yandex_transport", "yandextts"] +} diff --git a/homeassistant/brands/yeelight.json b/homeassistant/brands/yeelight.json new file mode 100644 index 00000000000..1ce04a99214 --- /dev/null +++ b/homeassistant/brands/yeelight.json @@ -0,0 +1,5 @@ +{ + "domain": "yeelight", + "name": "Yeelight", + "integrations": ["yeelight", "yeelightsunflower"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 155e0b58ce7..32cb12e6c36 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -109,11 +109,6 @@ "iot_class": "local_push", "name": "Alert" }, - "alexa": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Amazon Alexa" - }, "almond": { "config_flow": true, "iot_class": "local_polling", @@ -124,10 +119,30 @@ "iot_class": "cloud_polling", "name": "Alpha Vantage" }, - "amazon_polly": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Amazon Polly" + "amazon": { + "name": "Amazon", + "integrations": { + "alexa": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Amazon Alexa" + }, + "amazon_polly": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Amazon Polly" + }, + "aws": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Amazon Web Services (AWS)" + }, + "route53": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "AWS Route53" + } + } }, "amberelectric": { "config_flow": true, @@ -192,29 +207,34 @@ "apple": { "name": "Apple", "integrations": { - "icloud": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Apple iCloud" - }, - "ibeacon": { - "config_flow": true, - "iot_class": "local_push", - "name": "iBeacon Tracker" - }, "apple_tv": { "config_flow": true, "iot_class": "local_push", "name": "Apple TV" }, + "homekit_controller": { + "config_flow": true, + "iot_class": "local_push" + }, "homekit": { "config_flow": true, "iot_class": "local_push", "name": "HomeKit" }, - "homekit_controller": { + "ibeacon": { "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "name": "iBeacon Tracker" + }, + "icloud": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Apple iCloud" + }, + "itunes": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Apple iTunes" } } }, @@ -258,9 +278,19 @@ "name": "Arris TG2492LG" }, "aruba": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Aruba" + "name": "Aruba", + "integrations": { + "aruba": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Aruba" + }, + "cppm_tracker": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Aruba ClearPass" + } + } }, "arwn": { "config_flow": false, @@ -272,15 +302,20 @@ "iot_class": "cloud_polling", "name": "Aseko Pool Live" }, - "asterisk_cdr": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Asterisk Call Detail Records" - }, - "asterisk_mbox": { - "config_flow": false, - "iot_class": "local_push", - "name": "Asterisk Voicemail" + "asterisk": { + "name": "Asterisk", + "integrations": { + "asterisk_cdr": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Asterisk Call Detail Records" + }, + "asterisk_mbox": { + "config_flow": false, + "iot_class": "local_push", + "name": "Asterisk Voicemail" + } + } }, "asuswrt": { "config_flow": true, @@ -303,9 +338,19 @@ "name": "Atome Linky" }, "august": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "August" + "name": "August Home", + "integrations": { + "august": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "August" + }, + "yalexs_ble": { + "config_flow": true, + "iot_class": "local_push", + "name": "Yale Access Bluetooth" + } + } }, "aurora": { "config_flow": true, @@ -330,11 +375,6 @@ "config_flow": false, "iot_class": null }, - "avea": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Elgato Avea" - }, "avion": { "config_flow": false, "iot_class": "assumed_state", @@ -345,31 +385,11 @@ "iot_class": "local_polling", "name": "Awair" }, - "aws": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Amazon Web Services (AWS)" - }, "axis": { "config_flow": true, "iot_class": "local_push", "name": "Axis" }, - "azure_devops": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Azure DevOps" - }, - "azure_event_hub": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Azure Event Hub" - }, - "azure_service_bus": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Azure Service Bus" - }, "backup": { "config_flow": false, "iot_class": "calculated", @@ -494,11 +514,6 @@ "iot_class": "local_push", "name": "Bosch SHC" }, - "braviatv": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Sony Bravia TV" - }, "broadlink": { "config_flow": true, "iot_class": "local_polling", @@ -585,20 +600,25 @@ "iot_class": "cloud_push", "name": "Unify Circuit" }, - "cisco_ios": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Cisco IOS" - }, - "cisco_mobility_express": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Cisco Mobility Express" - }, - "cisco_webex_teams": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Cisco Webex Teams" + "cisco": { + "name": "Cisco", + "integrations": { + "cisco_ios": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Cisco IOS" + }, + "cisco_mobility_express": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Cisco Mobility Express" + }, + "cisco_webex_teams": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Cisco Webex Teams" + } + } }, "citybikes": { "config_flow": false, @@ -616,14 +636,19 @@ "name": "Clickatell" }, "clicksend": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "ClickSend SMS" - }, - "clicksend_tts": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "ClickSend TTS" + "name": "ClickSend", + "integrations": { + "clicksend": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "ClickSend SMS" + }, + "clicksend_tts": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "ClickSend TTS" + } + } }, "climate": { "config_flow": false, @@ -716,11 +741,6 @@ "config_flow": false, "iot_class": null }, - "cppm_tracker": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Aruba ClearPass" - }, "cpuspeed": { "config_flow": true, "iot_class": "local_push" @@ -843,15 +863,20 @@ "config_flow": false, "iot_class": null }, - "devolo_home_control": { - "config_flow": true, - "iot_class": "local_push", - "name": "devolo Home Control" - }, - "devolo_home_network": { - "config_flow": true, - "iot_class": "local_polling", - "name": "devolo Home Network" + "devolo": { + "name": "devolo", + "integrations": { + "devolo_home_control": { + "config_flow": true, + "iot_class": "local_push", + "name": "devolo Home Control" + }, + "devolo_home_network": { + "config_flow": true, + "iot_class": "local_polling", + "name": "devolo Home Network" + } + } }, "dexcom": { "config_flow": true, @@ -907,15 +932,20 @@ "iot_class": "local_polling", "name": "D-Link Wi-Fi Smart Plugs" }, - "dlna_dmr": { - "config_flow": true, - "iot_class": "local_push", - "name": "DLNA Digital Media Renderer" - }, - "dlna_dms": { - "config_flow": true, - "iot_class": "local_polling", - "name": "DLNA Digital Media Server" + "dlna": { + "name": "DLNA", + "integrations": { + "dlna_dmr": { + "config_flow": true, + "iot_class": "local_push", + "name": "DLNA Digital Media Renderer" + }, + "dlna_dms": { + "config_flow": true, + "iot_class": "local_polling", + "name": "DLNA Digital Media Server" + } + } }, "dnsip": { "config_flow": true, @@ -987,11 +1017,6 @@ "iot_class": "cloud_polling", "name": "dweet.io" }, - "dynalite": { - "config_flow": true, - "iot_class": "local_push", - "name": "Philips Dynalite" - }, "eafm": { "config_flow": true, "iot_class": "cloud_polling", @@ -1063,9 +1088,19 @@ "name": "Eight Sleep" }, "elgato": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Elgato Light" + "name": "Elgato", + "integrations": { + "avea": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Elgato Avea" + }, + "elgato": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Elgato Light" + } + } }, "eliqonline": { "config_flow": false, @@ -1093,14 +1128,19 @@ "name": "Emby" }, "emoncms": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Emoncms" - }, - "emoncms_history": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Emoncms History" + "name": "emoncms", + "integrations": { + "emoncms": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Emoncms" + }, + "emoncms_history": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Emoncms History" + } + } }, "emonitor": { "config_flow": true, @@ -1161,19 +1201,34 @@ "name": "EPH Controls" }, "epson": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Epson" + "name": "Epson", + "integrations": { + "epson": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Epson" + }, + "epsonworkforce": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Epson Workforce" + } + } }, - "epsonworkforce": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Epson Workforce" - }, - "eq3btsmart": { - "config_flow": false, - "iot_class": "local_polling", - "name": "eQ-3 Bluetooth Smart Thermostats" + "eq3": { + "name": "eQ-3", + "integrations": { + "eq3btsmart": { + "config_flow": false, + "iot_class": "local_polling", + "name": "eQ-3 Bluetooth Smart Thermostats" + }, + "maxcube": { + "config_flow": false, + "iot_class": "local_polling", + "name": "eQ-3 MAX!" + } + } }, "escea": { "config_flow": true, @@ -1205,11 +1260,6 @@ "iot_class": "local_polling", "name": "Evil Genius Labs" }, - "evohome": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Honeywell Total Connect Comfort (Europe)" - }, "ezviz": { "config_flow": true, "iot_class": "cloud_polling", @@ -1235,11 +1285,6 @@ "iot_class": "local_polling", "name": "Fail2Ban" }, - "familyhub": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Samsung Family Hub" - }, "fan": { "config_flow": false, "iot_class": null @@ -1255,19 +1300,24 @@ "name": "Feedreader" }, "ffmpeg": { - "config_flow": false, - "iot_class": null, - "name": "FFmpeg" - }, - "ffmpeg_motion": { - "config_flow": false, - "iot_class": "calculated", - "name": "FFmpeg Motion" - }, - "ffmpeg_noise": { - "config_flow": false, - "iot_class": "calculated", - "name": "FFmpeg Noise" + "name": "FFmpeg", + "integrations": { + "ffmpeg": { + "config_flow": false, + "iot_class": null, + "name": "FFmpeg" + }, + "ffmpeg_motion": { + "config_flow": false, + "iot_class": "calculated", + "name": "FFmpeg Motion" + }, + "ffmpeg_noise": { + "config_flow": false, + "iot_class": "calculated", + "name": "FFmpeg Noise" + } + } }, "fibaro": { "config_flow": true, @@ -1497,11 +1547,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "gc100": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Global Cach\u00e9 GC-100" - }, "gdacs": { "config_flow": true, "iot_class": "cloud_polling", @@ -1552,15 +1597,20 @@ "iot_class": "cloud_push", "name": "Geofency" }, - "geonetnz_quakes": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "GeoNet NZ Quakes" - }, - "geonetnz_volcano": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "GeoNet NZ Volcano" + "geonet": { + "name": "GeoNet", + "integrations": { + "geonetnz_quakes": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "GeoNet NZ Quakes" + }, + "geonetnz_volcano": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "GeoNet NZ Volcano" + } + } }, "gios": { "config_flow": true, @@ -1587,6 +1637,21 @@ "iot_class": "local_polling", "name": "Glances" }, + "globalcache": { + "name": "Global Cach\u00e9", + "integrations": { + "gc100": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Global Cach\u00e9 GC-100" + }, + "itach": { + "config_flow": false, + "iot_class": "assumed_state", + "name": "Global Cach\u00e9 iTach TCP/IP to IR" + } + } + }, "goalfeed": { "config_flow": false, "iot_class": "cloud_push", @@ -1750,11 +1815,6 @@ "iot_class": "local_polling", "name": "Harman Kardon AVR" }, - "harmony": { - "config_flow": true, - "iot_class": "local_push", - "name": "Logitech Harmony Hub" - }, "hassio": { "config_flow": false, "iot_class": "local_polling", @@ -1786,14 +1846,19 @@ "name": "HERE Travel Time" }, "hikvision": { - "config_flow": false, - "iot_class": "local_push", - "name": "Hikvision" - }, - "hikvisioncam": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Hikvision" + "name": "Hikvision", + "integrations": { + "hikvision": { + "config_flow": false, + "iot_class": "local_push", + "name": "Hikvision" + }, + "hikvisioncam": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Hikvision" + } + } }, "hisense_aehw4a1": { "config_flow": true, @@ -1846,29 +1911,44 @@ "name": "Home Assistant Alerts" }, "homematic": { - "config_flow": false, - "iot_class": "local_push", - "name": "Homematic" - }, - "homematicip_cloud": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "HomematicIP Cloud" + "name": "Homematic", + "integrations": { + "homematic": { + "config_flow": false, + "iot_class": "local_push", + "name": "Homematic" + }, + "homematicip_cloud": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "HomematicIP Cloud" + } + } }, "homewizard": { "config_flow": true, "iot_class": "local_polling", "name": "HomeWizard Energy" }, - "homeworks": { - "config_flow": false, - "iot_class": "local_push", - "name": "Lutron Homeworks" - }, "honeywell": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Honeywell Total Connect Comfort (US)" + "name": "Honeywell", + "integrations": { + "lyric": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Honeywell Lyric" + }, + "evohome": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Honeywell Total Connect Comfort (Europe)" + }, + "honeywell": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Honeywell Total Connect Comfort (US)" + } + } }, "horizon": { "config_flow": false, @@ -1895,11 +1975,6 @@ "iot_class": "local_polling", "name": "Huawei LTE" }, - "hue": { - "config_flow": true, - "iot_class": "local_push", - "name": "Philips Hue" - }, "huisbaasje": { "config_flow": true, "iot_class": "cloud_polling", @@ -1944,6 +2019,21 @@ "iot_class": "cloud_polling", "name": "Jandy iAqualink" }, + "ibm": { + "name": "IBM", + "integrations": { + "watson_iot": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "IBM Watson IoT Platform" + }, + "watson_tts": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "IBM Watson TTS" + } + } + }, "idteck_prox": { "config_flow": false, "iot_class": "local_push", @@ -2077,16 +2167,6 @@ "iot_class": "local_push", "name": "Universal Devices ISY994" }, - "itach": { - "config_flow": false, - "iot_class": "assumed_state", - "name": "Global Cach\u00e9 iTach TCP/IP to IR" - }, - "itunes": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Apple iTunes" - }, "izone": { "config_flow": true, "iot_class": "local_polling", @@ -2273,15 +2353,25 @@ "zwave" ] }, - "lg_netcast": { - "config_flow": false, - "iot_class": "local_polling", - "name": "LG Netcast" - }, - "lg_soundbar": { - "config_flow": true, - "iot_class": "local_polling", - "name": "LG Soundbars" + "lg": { + "name": "LG", + "integrations": { + "lg_netcast": { + "config_flow": false, + "iot_class": "local_polling", + "name": "LG Netcast" + }, + "lg_soundbar": { + "config_flow": true, + "iot_class": "local_polling", + "name": "LG Soundbars" + }, + "webostv": { + "config_flow": true, + "iot_class": "local_push", + "name": "LG webOS Smart TV" + } + } }, "lidarr": { "config_flow": true, @@ -2390,6 +2480,26 @@ "iot_class": "cloud_polling", "name": "Logi Circle" }, + "logitech": { + "name": "Logitech", + "integrations": { + "harmony": { + "config_flow": true, + "iot_class": "local_push", + "name": "Logitech Harmony Hub" + }, + "ue_smart_radio": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Logitech UE Smart Radio" + }, + "squeezebox": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Squeezebox (Logitech Media Server)" + } + } + }, "london_air": { "config_flow": false, "iot_class": "cloud_polling", @@ -2410,11 +2520,6 @@ "iot_class": null, "name": "Dashboards" }, - "luci": { - "config_flow": false, - "iot_class": "local_polling", - "name": "OpenWrt (luci)" - }, "luftdaten": { "config_flow": true, "iot_class": "cloud_polling", @@ -2426,25 +2531,30 @@ "name": "Lupus Electronics LUPUSEC" }, "lutron": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Lutron" - }, - "lutron_caseta": { - "config_flow": true, - "iot_class": "local_push", - "name": "Lutron Cas\u00e9ta" + "name": "Lutron", + "integrations": { + "lutron": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Lutron" + }, + "lutron_caseta": { + "config_flow": true, + "iot_class": "local_push", + "name": "Lutron Cas\u00e9ta" + }, + "homeworks": { + "config_flow": false, + "iot_class": "local_push", + "name": "Lutron Homeworks" + } + } }, "lw12wifi": { "config_flow": false, "iot_class": "local_polling", "name": "LAGUTE LW-12" }, - "lyric": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Honeywell Lyric" - }, "magicseaweed": { "config_flow": false, "iot_class": "cloud_polling", @@ -2464,11 +2574,6 @@ "iot_class": "calculated", "name": "Manual Alarm Control Panel" }, - "manual_mqtt": { - "config_flow": false, - "iot_class": "local_push", - "name": "Manual MQTT Alarm Control Panel" - }, "map": { "config_flow": false, "iot_class": null, @@ -2489,11 +2594,6 @@ "iot_class": "cloud_push", "name": "Matrix" }, - "maxcube": { - "config_flow": false, - "iot_class": "local_polling", - "name": "eQ-3 MAX!" - }, "mazda": { "config_flow": true, "iot_class": "cloud_polling", @@ -2534,9 +2634,19 @@ "name": "Melissa" }, "melnor": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Melnor Bluetooth" + "name": "Melnor", + "integrations": { + "melnor": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Melnor Bluetooth" + }, + "raincloud": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Melnor RainCloud" + } + } }, "meraki": { "config_flow": false, @@ -2584,24 +2694,59 @@ "name": "Ubiquiti mFi mPort" }, "microsoft": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Text-to-Speech (TTS)" - }, - "microsoft_face": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Face" - }, - "microsoft_face_detect": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Face Detect" - }, - "microsoft_face_identify": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Face Identify" + "name": "Microsoft", + "integrations": { + "azure_devops": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Azure DevOps" + }, + "azure_event_hub": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Azure Event Hub" + }, + "azure_service_bus": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Azure Service Bus" + }, + "microsoft_face_detect": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Face Detect" + }, + "microsoft_face_identify": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Face Identify" + }, + "microsoft_face": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Face" + }, + "microsoft": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Text-to-Speech (TTS)" + }, + "msteams": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Teams" + }, + "xbox": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Xbox" + }, + "xbox_live": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Xbox Live" + } + } }, "miflora": { "config_flow": false, @@ -2701,34 +2846,39 @@ "name": "Music Player Daemon (MPD)" }, "mqtt": { - "config_flow": true, - "iot_class": "local_push", - "name": "MQTT" - }, - "mqtt_eventstream": { - "config_flow": false, - "iot_class": "local_polling", - "name": "MQTT Eventstream" - }, - "mqtt_json": { - "config_flow": false, - "iot_class": "local_push", - "name": "MQTT JSON" - }, - "mqtt_room": { - "config_flow": false, - "iot_class": "local_push", - "name": "MQTT Room Presence" - }, - "mqtt_statestream": { - "config_flow": false, - "iot_class": "local_push", - "name": "MQTT Statestream" - }, - "msteams": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Teams" + "name": "MQTT", + "integrations": { + "manual_mqtt": { + "config_flow": false, + "iot_class": "local_push", + "name": "Manual MQTT Alarm Control Panel" + }, + "mqtt": { + "config_flow": true, + "iot_class": "local_push", + "name": "MQTT" + }, + "mqtt_eventstream": { + "config_flow": false, + "iot_class": "local_polling", + "name": "MQTT Eventstream" + }, + "mqtt_json": { + "config_flow": false, + "iot_class": "local_push", + "name": "MQTT JSON" + }, + "mqtt_room": { + "config_flow": false, + "iot_class": "local_push", + "name": "MQTT Room Presence" + }, + "mqtt_statestream": { + "config_flow": false, + "iot_class": "local_push", + "name": "MQTT Statestream" + } + } }, "mullvad": { "config_flow": true, @@ -2821,14 +2971,19 @@ "name": "Netdata" }, "netgear": { - "config_flow": true, - "iot_class": "local_polling", - "name": "NETGEAR" - }, - "netgear_lte": { - "config_flow": false, - "iot_class": "local_polling", - "name": "NETGEAR LTE" + "name": "NETGEAR", + "integrations": { + "netgear": { + "config_flow": true, + "iot_class": "local_polling", + "name": "NETGEAR" + }, + "netgear_lte": { + "config_flow": false, + "iot_class": "local_polling", + "name": "NETGEAR LTE" + } + } }, "netio": { "config_flow": false, @@ -3132,6 +3287,21 @@ "iot_class": "cloud_polling", "name": "OpenWeatherMap" }, + "openwrt": { + "name": "OpenWrt", + "integrations": { + "luci": { + "config_flow": false, + "iot_class": "local_polling", + "name": "OpenWrt (luci)" + }, + "ubus": { + "config_flow": false, + "iot_class": "local_polling", + "name": "OpenWrt (ubus)" + } + } + }, "opnsense": { "config_flow": false, "iot_class": "local_polling", @@ -3182,15 +3352,20 @@ "iot_class": "local_polling", "name": "P1 Monitor" }, - "panasonic_bluray": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Panasonic Blu-Ray Player" - }, - "panasonic_viera": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Panasonic Viera" + "panasonic": { + "name": "Panasonic", + "integrations": { + "panasonic_bluray": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Panasonic Blu-Ray Player" + }, + "panasonic_viera": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Panasonic Viera" + } + } }, "pandora": { "config_flow": false, @@ -3226,10 +3401,25 @@ "config_flow": false, "iot_class": "calculated" }, - "philips_js": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Philips TV" + "philips": { + "name": "Philips", + "integrations": { + "dynalite": { + "config_flow": true, + "iot_class": "local_push", + "name": "Philips Dynalite" + }, + "hue": { + "config_flow": true, + "iot_class": "local_push", + "name": "Philips Hue" + }, + "philips_js": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Philips TV" + } + } }, "pi_hole": { "config_flow": true, @@ -3305,11 +3495,6 @@ "iot_class": "cloud_polling", "name": "PoolSense" }, - "powerwall": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Tesla Powerwall" - }, "profiler": { "config_flow": true, "iot_class": null, @@ -3359,11 +3544,6 @@ "iot_class": "local_polling", "name": "PrusaLink" }, - "ps4": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Sony PlayStation 4" - }, "pulseaudio_loopback": { "config_flow": false, "iot_class": "local_polling", @@ -3430,14 +3610,19 @@ "name": "Queensland Bushfire Alert" }, "qnap": { - "config_flow": false, - "iot_class": "local_polling", - "name": "QNAP" - }, - "qnap_qsw": { - "config_flow": true, - "iot_class": "local_polling", - "name": "QNAP QSW" + "name": "QNAP", + "integrations": { + "qnap": { + "config_flow": false, + "iot_class": "local_polling", + "name": "QNAP" + }, + "qnap_qsw": { + "config_flow": true, + "iot_class": "local_polling", + "name": "QNAP QSW" + } + } }, "qrcode": { "config_flow": false, @@ -3484,11 +3669,6 @@ "iot_class": "local_polling", "name": "Rain Bird" }, - "raincloud": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Melnor RainCloud" - }, "rainforest_eagle": { "config_flow": true, "iot_class": "local_polling", @@ -3504,6 +3684,25 @@ "iot_class": "local_polling", "name": "Random" }, + "raspberry": { + "name": "Raspberry Pi", + "integrations": { + "rpi_camera": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Raspberry Pi Camera" + }, + "rpi_power": { + "config_flow": true, + "iot_class": "local_polling" + }, + "remote_rpi_gpio": { + "config_flow": false, + "iot_class": "local_push", + "name": "remote_rpi_gpio" + } + } + }, "raspyrfm": { "config_flow": false, "iot_class": "assumed_state", @@ -3548,11 +3747,6 @@ "config_flow": false, "iot_class": null }, - "remote_rpi_gpio": { - "config_flow": false, - "iot_class": "local_push", - "name": "remote_rpi_gpio" - }, "renault": { "config_flow": true, "iot_class": "cloud_polling", @@ -3643,25 +3837,11 @@ "iot_class": "local_push", "name": "RoonLabs music player" }, - "route53": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "AWS Route53" - }, "rova": { "config_flow": false, "iot_class": "cloud_polling", "name": "ROVA" }, - "rpi_camera": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Raspberry Pi Camera" - }, - "rpi_power": { - "config_flow": true, - "iot_class": "local_polling" - }, "rss_feed_template": { "config_flow": false, "iot_class": "local_push", @@ -3682,15 +3862,20 @@ "iot_class": "local_polling", "name": "Ruckus Unleashed" }, - "russound_rio": { - "config_flow": false, - "iot_class": "local_push", - "name": "Russound RIO" - }, - "russound_rnet": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Russound RNET" + "russound": { + "name": "Russound", + "integrations": { + "russound_rio": { + "config_flow": false, + "iot_class": "local_push", + "name": "Russound RIO" + }, + "russound_rnet": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Russound RNET" + } + } }, "sabnzbd": { "config_flow": true, @@ -3707,10 +3892,25 @@ "iot_class": "local_polling", "name": "SAJ Solar Inverter" }, - "samsungtv": { - "config_flow": true, - "iot_class": "local_push", - "name": "Samsung Smart TV" + "samsung": { + "name": "Samsung", + "integrations": { + "familyhub": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Samsung Family Hub" + }, + "samsungtv": { + "config_flow": true, + "iot_class": "local_push", + "name": "Samsung Smart TV" + }, + "syncthru": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Samsung SyncThru Printer" + } + } }, "satel_integra": { "config_flow": false, @@ -4002,14 +4202,19 @@ "name": "SNMP" }, "solaredge": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "SolarEdge" - }, - "solaredge_local": { - "config_flow": false, - "iot_class": "local_polling", - "name": "SolarEdge Local" + "name": "SolarEdge", + "integrations": { + "solaredge": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "SolarEdge" + }, + "solaredge_local": { + "config_flow": false, + "iot_class": "local_polling", + "name": "SolarEdge Local" + } + } }, "solarlog": { "config_flow": true, @@ -4036,20 +4241,35 @@ "iot_class": "local_polling", "name": "Sonarr" }, - "songpal": { - "config_flow": true, - "iot_class": "local_push", - "name": "Sony Songpal" - }, "sonos": { "config_flow": true, "iot_class": "local_push", "name": "Sonos" }, - "sony_projector": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Sony Projector" + "sony": { + "name": "Sony", + "integrations": { + "braviatv": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Sony Bravia TV" + }, + "ps4": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Sony PlayStation 4" + }, + "sony_projector": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Sony Projector" + }, + "songpal": { + "config_flow": true, + "iot_class": "local_push", + "name": "Sony Songpal" + } + } }, "soundtouch": { "config_flow": true, @@ -4091,11 +4311,6 @@ "iot_class": "local_polling", "name": "SQL" }, - "squeezebox": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Squeezebox (Logitech Media Server)" - }, "srp_energy": { "config_flow": true, "iot_class": "cloud_polling", @@ -4239,25 +4454,25 @@ "iot_class": "local_polling", "name": "Syncthing" }, - "syncthru": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Samsung SyncThru Printer" - }, - "synology_chat": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Synology Chat" - }, - "synology_dsm": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Synology DSM" - }, - "synology_srm": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Synology SRM" + "synology": { + "name": "Synology", + "integrations": { + "synology_chat": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Synology Chat" + }, + "synology_dsm": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Synology DSM" + }, + "synology_srm": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Synology SRM" + } + } }, "syslog": { "config_flow": false, @@ -4333,24 +4548,34 @@ "name": "The Energy Detective TED5000" }, "telegram": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Telegram" + "name": "Telegram", + "integrations": { + "telegram": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Telegram" + }, + "telegram_bot": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Telegram bot" + } + } }, - "telegram_bot": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Telegram bot" - }, - "tellduslive": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Telldus Live" - }, - "tellstick": { - "config_flow": false, - "iot_class": "assumed_state", - "name": "TellStick" + "telldus": { + "name": "Telldus", + "integrations": { + "tellduslive": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Telldus Live" + }, + "tellstick": { + "config_flow": false, + "iot_class": "assumed_state", + "name": "TellStick" + } + } }, "telnet": { "config_flow": false, @@ -4372,10 +4597,20 @@ "iot_class": "local_polling", "name": "TensorFlow" }, - "tesla_wall_connector": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Tesla Wall Connector" + "tesla": { + "name": "Tesla", + "integrations": { + "powerwall": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Tesla Powerwall" + }, + "tesla_wall_connector": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Tesla Wall Connector" + } + } }, "tfiac": { "config_flow": false, @@ -4523,20 +4758,25 @@ "iot_class": "local_polling", "name": "IKEA TR\u00c5DFRI" }, - "trafikverket_ferry": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Trafikverket Ferry" - }, - "trafikverket_train": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Trafikverket Train" - }, - "trafikverket_weatherstation": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Trafikverket Weather Station" + "trafikverket": { + "name": "Trafikverket", + "integrations": { + "trafikverket_ferry": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Trafikverket Ferry" + }, + "trafikverket_train": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Trafikverket Train" + }, + "trafikverket_weatherstation": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Trafikverket Weather Station" + } + } }, "transmission": { "config_flow": true, @@ -4574,19 +4814,24 @@ "name": "Twente Milieu" }, "twilio": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Twilio" - }, - "twilio_call": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Twilio Call" - }, - "twilio_sms": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Twilio SMS" + "name": "Twilio", + "integrations": { + "twilio": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Twilio" + }, + "twilio_call": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Twilio Call" + }, + "twilio_sms": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Twilio SMS" + } + } }, "twinkly": { "config_flow": true, @@ -4628,16 +4873,6 @@ } } }, - "ubus": { - "config_flow": false, - "iot_class": "local_polling", - "name": "OpenWrt (ubus)" - }, - "ue_smart_radio": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Logitech UE Smart Radio" - }, "uk_transport": { "config_flow": false, "iot_class": "cloud_polling", @@ -4781,14 +5016,19 @@ "name": "VIZIO SmartCast" }, "vlc": { - "config_flow": false, - "iot_class": "local_polling", - "name": "VLC media player" - }, - "vlc_telnet": { - "config_flow": true, - "iot_class": "local_polling", - "name": "VLC media player via Telnet" + "name": "VideoLAN", + "integrations": { + "vlc": { + "config_flow": false, + "iot_class": "local_polling", + "name": "VLC media player" + }, + "vlc_telnet": { + "config_flow": true, + "iot_class": "local_polling", + "name": "VLC media player via Telnet" + } + } }, "voicerss": { "config_flow": false, @@ -4850,16 +5090,6 @@ "iot_class": "cloud_polling", "name": "WaterFurnace" }, - "watson_iot": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "IBM Watson IoT Platform" - }, - "watson_tts": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "IBM Watson TTS" - }, "watttime": { "config_flow": true, "iot_class": "cloud_polling", @@ -4879,11 +5109,6 @@ "iot_class": null, "name": "Webhook" }, - "webostv": { - "config_flow": true, - "iot_class": "local_push", - "name": "LG webOS Smart TV" - }, "websocket_api": { "config_flow": false, "iot_class": null, @@ -4974,45 +5199,40 @@ "iot_class": "local_polling", "name": "Heyu X10" }, - "xbox": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Xbox" - }, - "xbox_live": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Xbox Live" - }, "xeoma": { "config_flow": false, "iot_class": "local_polling", "name": "Xeoma" }, "xiaomi": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Xiaomi" - }, - "xiaomi_aqara": { - "config_flow": true, - "iot_class": "local_push", - "name": "Xiaomi Gateway (Aqara)" - }, - "xiaomi_ble": { - "config_flow": true, - "iot_class": "local_push", - "name": "Xiaomi BLE" - }, - "xiaomi_miio": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Xiaomi Miio" - }, - "xiaomi_tv": { - "config_flow": false, - "iot_class": "assumed_state", - "name": "Xiaomi TV" + "name": "Xiaomi", + "integrations": { + "xiaomi_aqara": { + "config_flow": true, + "iot_class": "local_push", + "name": "Xiaomi Gateway (Aqara)" + }, + "xiaomi_ble": { + "config_flow": true, + "iot_class": "local_push", + "name": "Xiaomi BLE" + }, + "xiaomi_miio": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Xiaomi Miio" + }, + "xiaomi_tv": { + "config_flow": false, + "iot_class": "assumed_state", + "name": "Xiaomi TV" + }, + "xiaomi": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Xiaomi" + } + } }, "xmpp": { "config_flow": false, @@ -5024,15 +5244,25 @@ "iot_class": "local_polling", "name": "EZcontrol XS1" }, - "yale_smart_alarm": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Yale Smart Living" - }, - "yalexs_ble": { - "config_flow": true, - "iot_class": "local_push", - "name": "Yale Access Bluetooth" + "yale": { + "name": "Yale", + "integrations": { + "august": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "August" + }, + "yale_smart_alarm": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Yale Smart Living" + }, + "yalexs_ble": { + "config_flow": true, + "iot_class": "local_push", + "name": "Yale Access Bluetooth" + } + } }, "yamaha": { "config_flow": false, @@ -5044,25 +5274,35 @@ "iot_class": "local_push", "name": "MusicCast" }, - "yandex_transport": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Yandex Transport" - }, - "yandextts": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Yandex TTS" + "yandex": { + "name": "Yandex", + "integrations": { + "yandex_transport": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Yandex Transport" + }, + "yandextts": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Yandex TTS" + } + } }, "yeelight": { - "config_flow": true, - "iot_class": "local_push", - "name": "Yeelight" - }, - "yeelightsunflower": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Yeelight Sunflower" + "name": "Yeelight", + "integrations": { + "yeelight": { + "config_flow": true, + "iot_class": "local_push", + "name": "Yeelight" + }, + "yeelightsunflower": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Yeelight Sunflower" + } + } }, "yi": { "config_flow": false, From 09f1039f325637ce48d2da90f809c2780fa1e998 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Oct 2022 14:47:57 +0200 Subject: [PATCH 099/183] Add docstring to US volume constants (#79582) * Add docstring to US volume constants * A blank line separation --- homeassistant/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index 26deb059b99..7c78cf46b96 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -559,7 +559,10 @@ VOLUME_CUBIC_METERS: Final = "m³" VOLUME_CUBIC_FEET: Final = "ft³" VOLUME_GALLONS: Final = "gal" +"""US gallon (British gallon is not yet supported)""" + VOLUME_FLUID_OUNCE: Final = "fl. oz." +"""US fluid ounce (British fluid ounce is not yet supported)""" # Volume Flow Rate units VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = "m³/h" From 723d415966cfed5571c6cab78922af3fb7fb9833 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 Oct 2022 16:36:42 +0200 Subject: [PATCH 100/183] Set system & entity integration types (#79593) --- .../alarm_control_panel/manifest.json | 3 +- homeassistant/components/api/manifest.json | 3 +- .../application_credentials/manifest.json | 3 +- homeassistant/components/auth/manifest.json | 3 +- .../components/automation/manifest.json | 3 +- homeassistant/components/backup/manifest.json | 3 +- .../components/binary_sensor/manifest.json | 3 +- .../components/blueprint/manifest.json | 3 +- homeassistant/components/button/manifest.json | 3 +- .../components/calendar/manifest.json | 3 +- homeassistant/components/camera/manifest.json | 3 +- .../components/climate/manifest.json | 3 +- homeassistant/components/config/manifest.json | 3 +- .../components/configurator/manifest.json | 3 +- .../components/conversation/manifest.json | 3 +- homeassistant/components/cover/manifest.json | 3 +- .../components/default_config/manifest.json | 3 +- .../device_automation/manifest.json | 3 +- .../components/device_tracker/manifest.json | 3 +- homeassistant/components/dhcp/manifest.json | 3 +- .../components/diagnostics/manifest.json | 3 +- .../components/discovery/manifest.json | 3 +- homeassistant/components/energy/manifest.json | 3 +- homeassistant/components/fan/manifest.json | 3 +- .../components/file_upload/manifest.json | 3 +- .../components/frontend/manifest.json | 3 +- .../components/geo_location/manifest.json | 3 +- .../components/hardware/manifest.json | 3 +- homeassistant/components/hassio/manifest.json | 3 +- .../components/history/manifest.json | 3 +- .../components/homeassistant/manifest.json | 3 +- homeassistant/components/http/manifest.json | 3 +- .../components/humidifier/manifest.json | 3 +- homeassistant/components/image/manifest.json | 3 +- .../components/image_processing/manifest.json | 3 +- homeassistant/components/intent/manifest.json | 3 +- homeassistant/components/light/manifest.json | 3 +- homeassistant/components/lock/manifest.json | 3 +- .../components/logbook/manifest.json | 3 +- homeassistant/components/logger/manifest.json | 3 +- .../components/lovelace/manifest.json | 3 +- .../components/mailbox/manifest.json | 3 +- .../components/media_player/manifest.json | 3 +- .../components/media_source/manifest.json | 3 +- homeassistant/components/my/manifest.json | 3 +- .../components/network/manifest.json | 3 +- homeassistant/components/notify/manifest.json | 3 +- homeassistant/components/number/manifest.json | 3 +- .../components/onboarding/manifest.json | 3 +- homeassistant/components/person/manifest.json | 3 +- .../components/recorder/manifest.json | 3 +- homeassistant/components/remote/manifest.json | 3 +- .../components/repairs/manifest.json | 3 +- .../components/safe_mode/manifest.json | 3 +- homeassistant/components/scene/manifest.json | 3 +- homeassistant/components/script/manifest.json | 3 +- homeassistant/components/search/manifest.json | 3 +- homeassistant/components/select/manifest.json | 3 +- homeassistant/components/sensor/manifest.json | 3 +- homeassistant/components/siren/manifest.json | 3 +- homeassistant/components/ssdp/manifest.json | 3 +- homeassistant/components/stt/manifest.json | 3 +- homeassistant/components/switch/manifest.json | 3 +- .../components/system_health/manifest.json | 3 +- homeassistant/components/trace/manifest.json | 3 +- homeassistant/components/tts/manifest.json | 3 +- homeassistant/components/update/manifest.json | 3 +- homeassistant/components/usb/manifest.json | 3 +- homeassistant/components/vacuum/manifest.json | 3 +- .../components/water_heater/manifest.json | 3 +- .../components/weather/manifest.json | 3 +- .../components/websocket_api/manifest.json | 3 +- .../components/zeroconf/manifest.json | 3 +- homeassistant/generated/integrations.json | 365 ------------------ 74 files changed, 146 insertions(+), 438 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index 461094e8ce6..426e1e15afb 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -3,5 +3,6 @@ "name": "Alarm Control Panel", "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/api/manifest.json b/homeassistant/components/api/manifest.json index 1f400470943..dadfc95c3b9 100644 --- a/homeassistant/components/api/manifest.json +++ b/homeassistant/components/api/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/api", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/application_credentials/manifest.json b/homeassistant/components/application_credentials/manifest.json index 9a8abc16c36..fa45f1a6309 100644 --- a/homeassistant/components/application_credentials/manifest.json +++ b/homeassistant/components/application_credentials/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/application_credentials", "dependencies": ["auth", "websocket_api"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 2674bdfb032..200e41713d6 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/auth", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 9dd0130ee2f..3bfb192759c 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["blueprint", "trace"], "after_dependencies": ["device_automation", "webhook"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/backup/manifest.json b/homeassistant/components/backup/manifest.json index eaf6a9fd979..3eefa68fcc4 100644 --- a/homeassistant/components/backup/manifest.json +++ b/homeassistant/components/backup/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@home-assistant/core"], "requirements": ["securetar==2022.2.0"], "iot_class": "calculated", - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/binary_sensor/manifest.json b/homeassistant/components/binary_sensor/manifest.json index d1c631ee94b..e10478889f3 100644 --- a/homeassistant/components/binary_sensor/manifest.json +++ b/homeassistant/components/binary_sensor/manifest.json @@ -3,5 +3,6 @@ "name": "Binary Sensor", "documentation": "https://www.home-assistant.io/integrations/binary_sensor", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/blueprint/manifest.json b/homeassistant/components/blueprint/manifest.json index c00b92b1e3c..4ed299438bb 100644 --- a/homeassistant/components/blueprint/manifest.json +++ b/homeassistant/components/blueprint/manifest.json @@ -3,5 +3,6 @@ "name": "Blueprint", "documentation": "https://www.home-assistant.io/integrations/blueprint", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/button/manifest.json b/homeassistant/components/button/manifest.json index beeaca487a6..02945d979ff 100644 --- a/homeassistant/components/button/manifest.json +++ b/homeassistant/components/button/manifest.json @@ -3,5 +3,6 @@ "name": "Button", "documentation": "https://www.home-assistant.io/integrations/button", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 2fb4df84414..cc4f09cfa64 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/calendar", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index b1ab479f3a5..92bed21c1b8 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -6,5 +6,6 @@ "requirements": ["PyTurboJPEG==1.6.7"], "after_dependencies": ["media_player"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/climate/manifest.json b/homeassistant/components/climate/manifest.json index 8b54d3a91ad..7c23705181a 100644 --- a/homeassistant/components/climate/manifest.json +++ b/homeassistant/components/climate/manifest.json @@ -3,5 +3,6 @@ "name": "Climate", "documentation": "https://www.home-assistant.io/integrations/climate", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json index 57dfd0d360a..3be667f6cd2 100644 --- a/homeassistant/components/config/manifest.json +++ b/homeassistant/components/config/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/config", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/configurator/manifest.json b/homeassistant/components/configurator/manifest.json index acd0fa80423..716fe26910b 100644 --- a/homeassistant/components/configurator/manifest.json +++ b/homeassistant/components/configurator/manifest.json @@ -3,5 +3,6 @@ "name": "Configurator", "documentation": "https://www.home-assistant.io/integrations/configurator", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 1d2e0893065..54265bfcb83 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/cover/manifest.json b/homeassistant/components/cover/manifest.json index 3da130fd799..66347b77eea 100644 --- a/homeassistant/components/cover/manifest.json +++ b/homeassistant/components/cover/manifest.json @@ -3,5 +3,6 @@ "name": "Cover", "documentation": "https://www.home-assistant.io/integrations/cover", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 6701e62c71f..d33aee6e030 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -41,5 +41,6 @@ "zone" ], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/device_automation/manifest.json b/homeassistant/components/device_automation/manifest.json index 033a54312be..e897cb5a29f 100644 --- a/homeassistant/components/device_automation/manifest.json +++ b/homeassistant/components/device_automation/manifest.json @@ -3,5 +3,6 @@ "name": "Device Automation", "documentation": "https://www.home-assistant.io/integrations/device_automation", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 7abd68b03e2..1ce4349e537 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["zone"], "after_dependencies": [], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index f3f44f6dc9b..2ebb0fd63e0 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", - "loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"] + "loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"], + "integration_type": "system" } diff --git a/homeassistant/components/diagnostics/manifest.json b/homeassistant/components/diagnostics/manifest.json index ad6edf110b0..383ebebd947 100644 --- a/homeassistant/components/diagnostics/manifest.json +++ b/homeassistant/components/diagnostics/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/diagnostics", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 6f97993c788..c98cdfa60a6 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["zeroconf"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "loggers": ["netdisco"] + "loggers": ["netdisco"], + "integration_type": "system" } diff --git a/homeassistant/components/energy/manifest.json b/homeassistant/components/energy/manifest.json index 5ddc6457a61..39a3f66d65c 100644 --- a/homeassistant/components/energy/manifest.json +++ b/homeassistant/components/energy/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@home-assistant/core"], "iot_class": "calculated", "dependencies": ["websocket_api", "history", "recorder"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index bb968240f0b..f25162d9959 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -3,5 +3,6 @@ "name": "Fan", "documentation": "https://www.home-assistant.io/integrations/fan", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/file_upload/manifest.json b/homeassistant/components/file_upload/manifest.json index 6e190ba3712..d2b4f88a279 100644 --- a/homeassistant/components/file_upload/manifest.json +++ b/homeassistant/components/file_upload/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/file_upload", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index fde637657dd..62bed3777a8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -19,5 +19,6 @@ "websocket_api" ], "codeowners": ["@home-assistant/frontend"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index 2e0d7061099..3a0cb7eae91 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -3,5 +3,6 @@ "name": "Geolocation", "documentation": "https://www.home-assistant.io/integrations/geo_location", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/hardware/manifest.json b/homeassistant/components/hardware/manifest.json index 710726d9869..8f7e27e6911 100644 --- a/homeassistant/components/hardware/manifest.json +++ b/homeassistant/components/hardware/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/hardware", "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "requirements": ["psutil-home-assistant==0.0.1"] + "requirements": ["psutil-home-assistant==0.0.1"], + "integration_type": "system" } diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 5de80fdbd19..b087eb25807 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["panel_custom"], "codeowners": ["@home-assistant/supervisor"], "iot_class": "local_polling", - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/history/manifest.json b/homeassistant/components/history/manifest.json index 7185a8b63c4..4ebf64dd603 100644 --- a/homeassistant/components/history/manifest.json +++ b/homeassistant/components/history/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/history", "dependencies": ["http", "recorder"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/homeassistant/manifest.json b/homeassistant/components/homeassistant/manifest.json index 027d1b9376d..179e8deb233 100644 --- a/homeassistant/components/homeassistant/manifest.json +++ b/homeassistant/components/homeassistant/manifest.json @@ -3,5 +3,6 @@ "name": "Home Assistant Core Integration", "documentation": "https://www.home-assistant.io/integrations/homeassistant", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 4391fd1acaf..26bf3dc31ce 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -5,5 +5,6 @@ "requirements": ["aiohttp_cors==0.7.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/humidifier/manifest.json b/homeassistant/components/humidifier/manifest.json index b64065a2583..0cb84e08f0e 100644 --- a/homeassistant/components/humidifier/manifest.json +++ b/homeassistant/components/humidifier/manifest.json @@ -3,5 +3,6 @@ "name": "Humidifier", "documentation": "https://www.home-assistant.io/integrations/humidifier", "codeowners": ["@home-assistant/core", "@Shulyaka"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 4f967dbcc89..ed500c89011 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pillow==9.2.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 0315a69b82a..43a52268881 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/image_processing", "dependencies": ["camera"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/intent/manifest.json b/homeassistant/components/intent/manifest.json index e5c87461022..771482d76a4 100644 --- a/homeassistant/components/intent/manifest.json +++ b/homeassistant/components/intent/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/intent", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index c7cf2abc7c8..e49701794d4 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -3,5 +3,6 @@ "name": "Light", "documentation": "https://www.home-assistant.io/integrations/light", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index f93d2962ea3..0a786c05865 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -3,5 +3,6 @@ "name": "Lock", "documentation": "https://www.home-assistant.io/integrations/lock", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 66c0348a2ac..5b8a8d4c2a3 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/logger/manifest.json b/homeassistant/components/logger/manifest.json index 2cb04538260..ef0a6fa2e65 100644 --- a/homeassistant/components/logger/manifest.json +++ b/homeassistant/components/logger/manifest.json @@ -3,5 +3,6 @@ "name": "Logger", "documentation": "https://www.home-assistant.io/integrations/logger", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index 8cccf65f37c..7d9561f9755 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -3,5 +3,6 @@ "name": "Dashboards", "documentation": "https://www.home-assistant.io/integrations/lovelace", "codeowners": ["@home-assistant/frontend"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/mailbox/manifest.json b/homeassistant/components/mailbox/manifest.json index 9d8a1403332..8d080888985 100644 --- a/homeassistant/components/mailbox/manifest.json +++ b/homeassistant/components/mailbox/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mailbox", "dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/media_player/manifest.json b/homeassistant/components/media_player/manifest.json index 118d05036cc..4b8b9013b98 100644 --- a/homeassistant/components/media_player/manifest.json +++ b/homeassistant/components/media_player/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/media_player", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/media_source/manifest.json b/homeassistant/components/media_source/manifest.json index 3b00df4300b..ae65137c113 100644 --- a/homeassistant/components/media_source/manifest.json +++ b/homeassistant/components/media_source/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/media_source", "dependencies": ["http"], "codeowners": ["@hunterjm"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/my/manifest.json b/homeassistant/components/my/manifest.json index 8c88b092e1c..23d1b3d21e2 100644 --- a/homeassistant/components/my/manifest.json +++ b/homeassistant/components/my/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/my", "dependencies": ["frontend"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/network/manifest.json b/homeassistant/components/network/manifest.json index 9f2fa7849f0..40712a40faf 100644 --- a/homeassistant/components/network/manifest.json +++ b/homeassistant/components/network/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@home-assistant/core"], "dependencies": ["websocket_api"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/notify/manifest.json b/homeassistant/components/notify/manifest.json index b32295a10a6..3e930b32ba6 100644 --- a/homeassistant/components/notify/manifest.json +++ b/homeassistant/components/notify/manifest.json @@ -3,5 +3,6 @@ "name": "Notifications", "documentation": "https://www.home-assistant.io/integrations/notify", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/number/manifest.json b/homeassistant/components/number/manifest.json index 549494fa3f5..4cb16c8e0c3 100644 --- a/homeassistant/components/number/manifest.json +++ b/homeassistant/components/number/manifest.json @@ -3,5 +3,6 @@ "name": "Number", "documentation": "https://www.home-assistant.io/integrations/number", "codeowners": ["@home-assistant/core", "@Shulyaka"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index fe65d82f626..4e200d22502 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -5,5 +5,6 @@ "after_dependencies": ["hassio"], "dependencies": ["analytics", "auth", "http", "person"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index 09b74bf34eb..dc9c76ca103 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["device_tracker"], "codeowners": [], "quality_scale": "internal", - "iot_class": "calculated" + "iot_class": "calculated", + "integration_type": "system" } diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 51fd4a6dbe3..19a22b2a1e3 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -5,5 +5,6 @@ "requirements": ["sqlalchemy==1.4.41", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index d08cb624c16..dac51e27749 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -3,5 +3,6 @@ "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/repairs/manifest.json b/homeassistant/components/repairs/manifest.json index c87d9d559e0..c63c3ec2946 100644 --- a/homeassistant/components/repairs/manifest.json +++ b/homeassistant/components/repairs/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/repairs", "codeowners": ["@home-assistant/core"], "dependencies": ["http"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/safe_mode/manifest.json b/homeassistant/components/safe_mode/manifest.json index 5ce7c3abf7b..f2627693f33 100644 --- a/homeassistant/components/safe_mode/manifest.json +++ b/homeassistant/components/safe_mode/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/safe_mode", "dependencies": ["frontend", "persistent_notification", "cloud"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/scene/manifest.json b/homeassistant/components/scene/manifest.json index 3134a310042..d653c8076e6 100644 --- a/homeassistant/components/scene/manifest.json +++ b/homeassistant/components/scene/manifest.json @@ -3,5 +3,6 @@ "name": "Scenes", "documentation": "https://www.home-assistant.io/integrations/scene", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index da7d249ce12..a31861cba87 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/script", "dependencies": ["blueprint", "trace"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/search/manifest.json b/homeassistant/components/search/manifest.json index b9ce2115112..8feba7c08e2 100644 --- a/homeassistant/components/search/manifest.json +++ b/homeassistant/components/search/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["websocket_api"], "after_dependencies": ["scene", "group", "automation", "script"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/select/manifest.json b/homeassistant/components/select/manifest.json index 86e8b917199..8427a72321e 100644 --- a/homeassistant/components/select/manifest.json +++ b/homeassistant/components/select/manifest.json @@ -3,5 +3,6 @@ "name": "Select", "documentation": "https://www.home-assistant.io/integrations/select", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/sensor/manifest.json b/homeassistant/components/sensor/manifest.json index 4726ac790a7..f2057cf3012 100644 --- a/homeassistant/components/sensor/manifest.json +++ b/homeassistant/components/sensor/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sensor", "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "after_dependencies": ["recorder"] + "after_dependencies": ["recorder"], + "integration_type": "entity" } diff --git a/homeassistant/components/siren/manifest.json b/homeassistant/components/siren/manifest.json index a3f3989e3f1..58b16ed6880 100644 --- a/homeassistant/components/siren/manifest.json +++ b/homeassistant/components/siren/manifest.json @@ -3,5 +3,6 @@ "name": "Siren", "documentation": "https://www.home-assistant.io/integrations/siren", "codeowners": ["@home-assistant/core", "@raman325"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 88e3d0f4286..e403867226a 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -8,5 +8,6 @@ "codeowners": [], "quality_scale": "internal", "iot_class": "local_push", - "loggers": ["async_upnp_client"] + "loggers": ["async_upnp_client"], + "integration_type": "system" } diff --git a/homeassistant/components/stt/manifest.json b/homeassistant/components/stt/manifest.json index 43c5c8684a3..2d9da38af89 100644 --- a/homeassistant/components/stt/manifest.json +++ b/homeassistant/components/stt/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/stt", "dependencies": ["http"], "codeowners": ["@pvizeli"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index 929c617e46a..d2f64327285 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/switch", "after_dependencies": ["switch_as_x"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/system_health/manifest.json b/homeassistant/components/system_health/manifest.json index 4109855d466..9fb033abc21 100644 --- a/homeassistant/components/system_health/manifest.json +++ b/homeassistant/components/system_health/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/system_health", "dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/trace/manifest.json b/homeassistant/components/trace/manifest.json index 572dff17b03..79164268c73 100644 --- a/homeassistant/components/trace/manifest.json +++ b/homeassistant/components/trace/manifest.json @@ -3,5 +3,6 @@ "name": "Trace", "documentation": "https://www.home-assistant.io/integrations/automation", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index f81d112e825..f3b16cafac5 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -7,5 +7,6 @@ "after_dependencies": ["media_player"], "codeowners": ["@pvizeli"], "quality_scale": "internal", - "loggers": ["mutagen"] + "loggers": ["mutagen"], + "integration_type": "entity" } diff --git a/homeassistant/components/update/manifest.json b/homeassistant/components/update/manifest.json index f5fe74c9d02..44535a5d998 100644 --- a/homeassistant/components/update/manifest.json +++ b/homeassistant/components/update/manifest.json @@ -3,5 +3,6 @@ "name": "Update", "documentation": "https://www.home-assistant.io/integrations/update", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/usb/manifest.json b/homeassistant/components/usb/manifest.json index 22dca558379..792be3dcb59 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@bdraco"], "dependencies": ["websocket_api"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index ee4fa6a471e..28737a59750 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -3,5 +3,6 @@ "name": "Vacuum", "documentation": "https://www.home-assistant.io/integrations/vacuum", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/water_heater/manifest.json b/homeassistant/components/water_heater/manifest.json index ac00bc64210..63f9f513847 100644 --- a/homeassistant/components/water_heater/manifest.json +++ b/homeassistant/components/water_heater/manifest.json @@ -3,5 +3,6 @@ "name": "Water Heater", "documentation": "https://www.home-assistant.io/integrations/water_heater", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/weather/manifest.json b/homeassistant/components/weather/manifest.json index cbf04af989d..6d1d1665124 100644 --- a/homeassistant/components/weather/manifest.json +++ b/homeassistant/components/weather/manifest.json @@ -3,5 +3,6 @@ "name": "Weather", "documentation": "https://www.home-assistant.io/integrations/weather", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/websocket_api/manifest.json b/homeassistant/components/websocket_api/manifest.json index 66dd76af769..f40d2940561 100644 --- a/homeassistant/components/websocket_api/manifest.json +++ b/homeassistant/components/websocket_api/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/websocket_api", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index ec670558e66..5fcb514ea51 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", - "loggers": ["zeroconf"] + "loggers": ["zeroconf"], + "integration_type": "system" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 32cb12e6c36..0fc1f4c2f01 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -95,10 +95,6 @@ "iot_class": "cloud_polling", "name": "Aladdin Connect" }, - "alarm_control_panel": { - "config_flow": false, - "iot_class": null - }, "alarmdecoder": { "config_flow": true, "iot_class": "local_push", @@ -199,11 +195,6 @@ "iot_class": "local_polling", "name": "APC UPS Daemon" }, - "api": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant API" - }, "apple": { "name": "Apple", "integrations": { @@ -238,10 +229,6 @@ } } }, - "application_credentials": { - "config_flow": false, - "iot_class": null - }, "apprise": { "config_flow": false, "iot_class": "cloud_push", @@ -366,15 +353,6 @@ "iot_class": "cloud_polling", "name": "Aussie Broadband" }, - "auth": { - "config_flow": false, - "iot_class": null, - "name": "Auth" - }, - "automation": { - "config_flow": false, - "iot_class": null - }, "avion": { "config_flow": false, "iot_class": "assumed_state", @@ -390,11 +368,6 @@ "iot_class": "local_push", "name": "Axis" }, - "backup": { - "config_flow": false, - "iot_class": "calculated", - "name": "Backup" - }, "baf": { "config_flow": true, "iot_class": "local_push", @@ -425,10 +398,6 @@ "iot_class": "local_polling", "name": "BeeWi SmartClim BLE sensor" }, - "binary_sensor": { - "config_flow": false, - "iot_class": null - }, "bitcoin": { "config_flow": false, "iot_class": "cloud_polling", @@ -474,11 +443,6 @@ "iot_class": "local_push", "name": "BlueMaestro" }, - "blueprint": { - "config_flow": false, - "iot_class": null, - "name": "Blueprint" - }, "bluesound": { "config_flow": false, "iot_class": "local_polling", @@ -564,23 +528,11 @@ "iot_class": "cloud_polling", "name": "Buienradar" }, - "button": { - "config_flow": false, - "iot_class": null - }, "caldav": { "config_flow": false, "iot_class": "cloud_polling", "name": "CalDAV" }, - "calendar": { - "config_flow": false, - "iot_class": null - }, - "camera": { - "config_flow": false, - "iot_class": null - }, "canary": { "config_flow": true, "iot_class": "cloud_polling", @@ -650,10 +602,6 @@ } } }, - "climate": { - "config_flow": false, - "iot_class": null - }, "cloud": { "config_flow": false, "iot_class": "cloud_push", @@ -709,24 +657,11 @@ "iot_class": "local_polling", "name": "Concord232" }, - "config": { - "config_flow": false, - "iot_class": null, - "name": "Configuration" - }, - "configurator": { - "config_flow": false, - "iot_class": null - }, "control4": { "config_flow": true, "iot_class": "local_polling", "name": "Control4" }, - "conversation": { - "config_flow": false, - "iot_class": "local_push" - }, "coolmaster": { "config_flow": true, "iot_class": "local_polling", @@ -737,10 +672,6 @@ "iot_class": "cloud_polling", "name": "Coronavirus (COVID-19)" }, - "cover": { - "config_flow": false, - "iot_class": null - }, "cpuspeed": { "config_flow": true, "iot_class": "local_push" @@ -805,11 +736,6 @@ "iot_class": "cloud_polling", "name": "Leviton Decora Wi-Fi" }, - "default_config": { - "config_flow": false, - "iot_class": null, - "name": "Default Config" - }, "delijn": { "config_flow": false, "iot_class": "cloud_polling", @@ -849,20 +775,11 @@ "iot_class": "cloud_polling", "name": "Deutsche Bahn" }, - "device_automation": { - "config_flow": false, - "iot_class": null, - "name": "Device Automation" - }, "device_sun_light_trigger": { "config_flow": false, "iot_class": "calculated", "name": "Presence-based Lights" }, - "device_tracker": { - "config_flow": false, - "iot_class": null - }, "devolo": { "name": "devolo", "integrations": { @@ -883,15 +800,6 @@ "iot_class": "cloud_polling", "name": "Dexcom" }, - "dhcp": { - "config_flow": false, - "iot_class": "local_push", - "name": "DHCP Discovery" - }, - "diagnostics": { - "config_flow": false, - "iot_class": null - }, "digital_ocean": { "config_flow": false, "iot_class": "local_polling", @@ -912,11 +820,6 @@ "iot_class": "cloud_push", "name": "Discord" }, - "discovery": { - "config_flow": false, - "iot_class": null, - "name": "Discovery" - }, "dlib_face_detect": { "config_flow": false, "iot_class": "local_push", @@ -1161,10 +1064,6 @@ "config_flow": true, "iot_class": "local_push" }, - "energy": { - "config_flow": false, - "iot_class": "calculated" - }, "enigma2": { "config_flow": false, "iot_class": "local_polling", @@ -1285,10 +1184,6 @@ "iot_class": "local_polling", "name": "Fail2Ban" }, - "fan": { - "config_flow": false, - "iot_class": null - }, "fastdotcom": { "config_flow": false, "iot_class": "cloud_polling", @@ -1334,11 +1229,6 @@ "iot_class": "local_polling", "name": "File" }, - "file_upload": { - "config_flow": false, - "iot_class": null, - "name": "File Upload" - }, "filesize": { "config_flow": true, "iot_class": "local_polling" @@ -1518,11 +1408,6 @@ "iot_class": "local_polling", "name": "Fronius" }, - "frontend": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant Frontend" - }, "frontier_silicon": { "config_flow": false, "iot_class": "local_polling", @@ -1577,11 +1462,6 @@ "iot_class": "cloud_polling", "name": "GeoJSON" }, - "geo_location": { - "config_flow": false, - "iot_class": null, - "name": "Geolocation" - }, "geo_rss_events": { "config_flow": false, "iot_class": "cloud_polling", @@ -1805,21 +1685,11 @@ "iot_class": "cloud_polling", "name": "Habitica" }, - "hardware": { - "config_flow": false, - "iot_class": null, - "name": "Hardware" - }, "harman_kardon_avr": { "config_flow": false, "iot_class": "local_polling", "name": "Harman Kardon AVR" }, - "hassio": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Home Assistant Supervisor" - }, "haveibeenpwned": { "config_flow": false, "iot_class": "cloud_polling", @@ -1865,11 +1735,6 @@ "iot_class": "local_polling", "name": "Hisense AEH-W4A1" }, - "history": { - "config_flow": false, - "iot_class": null, - "name": "History" - }, "history_stats": { "config_flow": false, "iot_class": "local_polling", @@ -1900,11 +1765,6 @@ "iot_class": "cloud_polling", "name": "Legrand Home+ Control" }, - "homeassistant": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant Core Integration" - }, "homeassistant_alerts": { "config_flow": false, "iot_class": null, @@ -1965,11 +1825,6 @@ "iot_class": "cloud_push", "name": "HTML5 Push Notifications" }, - "http": { - "config_flow": false, - "iot_class": "local_push", - "name": "HTTP" - }, "huawei_lte": { "config_flow": true, "iot_class": "local_polling", @@ -1980,10 +1835,6 @@ "iot_class": "cloud_polling", "name": "Huisbaasje" }, - "humidifier": { - "config_flow": false, - "iot_class": null - }, "hunterdouglas_powerview": { "config_flow": true, "iot_class": "local_polling", @@ -2059,15 +1910,6 @@ "iot_class": "local_push", "name": "IHC Controller" }, - "image": { - "config_flow": false, - "iot_class": null, - "name": "Image" - }, - "image_processing": { - "config_flow": false, - "iot_class": null - }, "imap": { "config_flow": false, "iot_class": "cloud_push", @@ -2103,11 +1945,6 @@ "iot_class": "local_polling", "name": "IntelliFire" }, - "intent": { - "config_flow": false, - "iot_class": null, - "name": "Intent" - }, "intent_script": { "config_flow": false, "iot_class": null, @@ -2393,10 +2230,6 @@ "iot_class": "cloud_push", "name": "LIFX Cloud" }, - "light": { - "config_flow": false, - "iot_class": null - }, "lightwave": { "config_flow": false, "iot_class": "assumed_state", @@ -2456,25 +2289,11 @@ "iot_class": "local_push", "name": "Locative" }, - "lock": { - "config_flow": false, - "iot_class": null - }, - "logbook": { - "config_flow": false, - "iot_class": null, - "name": "Logbook" - }, "logentries": { "config_flow": false, "iot_class": "cloud_push", "name": "Logentries" }, - "logger": { - "config_flow": false, - "iot_class": null, - "name": "Logger" - }, "logi_circle": { "config_flow": true, "iot_class": "cloud_polling", @@ -2515,11 +2334,6 @@ "iot_class": "local_push", "name": "LOOKin" }, - "lovelace": { - "config_flow": false, - "iot_class": null, - "name": "Dashboards" - }, "luftdaten": { "config_flow": true, "iot_class": "cloud_polling", @@ -2560,10 +2374,6 @@ "iot_class": "cloud_polling", "name": "Magicseaweed" }, - "mailbox": { - "config_flow": false, - "iot_class": null - }, "mailgun": { "config_flow": true, "iot_class": "cloud_push", @@ -2609,15 +2419,6 @@ "iot_class": "calculated", "name": "Media Extractor" }, - "media_player": { - "config_flow": false, - "iot_class": null - }, - "media_source": { - "config_flow": false, - "iot_class": null, - "name": "Media Source" - }, "mediaroom": { "config_flow": false, "iot_class": "local_polling", @@ -2895,11 +2696,6 @@ "iot_class": "cloud_polling", "name": "MVG" }, - "my": { - "config_flow": false, - "iot_class": null, - "name": "My Home Assistant" - }, "mycroft": { "config_flow": false, "iot_class": "local_push", @@ -2990,11 +2786,6 @@ "iot_class": "local_polling", "name": "Netio" }, - "network": { - "config_flow": false, - "iot_class": "local_push", - "name": "Network Configuration" - }, "neurio_energy": { "config_flow": false, "iot_class": "cloud_polling", @@ -3084,10 +2875,6 @@ "iot_class": "cloud_polling", "name": "Om Luftkvalitet i Norge (Norway Air)" }, - "notify": { - "config_flow": false, - "iot_class": null - }, "notify_events": { "config_flow": false, "iot_class": "cloud_push", @@ -3123,10 +2910,6 @@ "iot_class": "local_push", "name": "Numato USB GPIO Expander" }, - "number": { - "config_flow": false, - "iot_class": null - }, "nut": { "config_flow": true, "iot_class": "local_polling", @@ -3182,11 +2965,6 @@ "iot_class": "cloud_polling", "name": "Hayward Omnilogic" }, - "onboarding": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant Onboarding" - }, "oncue": { "config_flow": true, "iot_class": "cloud_polling", @@ -3397,10 +3175,6 @@ "iot_class": "local_push", "name": "Persistent Notification" }, - "person": { - "config_flow": false, - "iot_class": "calculated" - }, "philips": { "name": "Philips", "integrations": { @@ -3718,11 +3492,6 @@ "iot_class": "cloud_polling", "name": "ReCollect Waste" }, - "recorder": { - "config_flow": false, - "iot_class": "local_push", - "name": "Recorder" - }, "recswitch": { "config_flow": false, "iot_class": "local_polling", @@ -3743,20 +3512,11 @@ "iot_class": "cloud_push", "name": "Remember The Milk" }, - "remote": { - "config_flow": false, - "iot_class": null - }, "renault": { "config_flow": true, "iot_class": "cloud_polling", "name": "Renault" }, - "repairs": { - "config_flow": false, - "iot_class": null, - "name": "Repairs" - }, "repetier": { "config_flow": false, "iot_class": "local_polling", @@ -3882,11 +3642,6 @@ "iot_class": "local_polling", "name": "SABnzbd" }, - "safe_mode": { - "config_flow": false, - "iot_class": null, - "name": "Safe Mode" - }, "saj": { "config_flow": false, "iot_class": "local_polling", @@ -3917,10 +3672,6 @@ "iot_class": "local_push", "name": "Satel Integra" }, - "scene": { - "config_flow": false, - "iot_class": null - }, "schluter": { "config_flow": false, "iot_class": "cloud_polling", @@ -3936,29 +3687,16 @@ "iot_class": "local_polling", "name": "Pentair ScreenLogic" }, - "script": { - "config_flow": false, - "iot_class": null - }, "scsgate": { "config_flow": false, "iot_class": "local_polling", "name": "SCSGate" }, - "search": { - "config_flow": false, - "iot_class": null, - "name": "Search" - }, "season": { "config_flow": true, "iot_class": "local_polling", "name": "Season" }, - "select": { - "config_flow": false, - "iot_class": null - }, "sendgrid": { "config_flow": false, "iot_class": "cloud_push", @@ -3979,10 +3717,6 @@ "iot_class": "cloud_polling", "name": "Sensibo" }, - "sensor": { - "config_flow": false, - "iot_class": null - }, "sensorpro": { "config_flow": true, "iot_class": "local_push", @@ -4097,10 +3831,6 @@ "iot_class": "cloud_push", "name": "Sinch SMS" }, - "siren": { - "config_flow": false, - "iot_class": null - }, "sisyphus": { "config_flow": false, "iot_class": "local_push", @@ -4316,11 +4046,6 @@ "iot_class": "cloud_polling", "name": "SRP Energy" }, - "ssdp": { - "config_flow": false, - "iot_class": "local_push", - "name": "Simple Service Discovery Protocol (SSDP)" - }, "starline": { "config_flow": true, "iot_class": "cloud_polling", @@ -4376,11 +4101,6 @@ "iot_class": "cloud_polling", "name": "StreamLabs" }, - "stt": { - "config_flow": false, - "iot_class": null, - "name": "Speech-to-Text (STT)" - }, "subaru": { "config_flow": true, "iot_class": "cloud_polling", @@ -4425,10 +4145,6 @@ "iot_class": "local_polling", "name": "Swisscom Internet-Box" }, - "switch": { - "config_flow": false, - "iot_class": null - }, "switchbee": { "config_flow": true, "iot_class": "local_polling", @@ -4484,10 +4200,6 @@ "iot_class": "local_push", "name": "System Bridge" }, - "system_health": { - "config_flow": false, - "iot_class": null - }, "system_log": { "config_flow": false, "iot_class": null, @@ -4743,11 +4455,6 @@ "iot_class": "local_polling", "name": "Traccar" }, - "trace": { - "config_flow": false, - "iot_class": null, - "name": "Trace" - }, "tractive": { "config_flow": true, "iot_class": "cloud_push", @@ -4798,11 +4505,6 @@ "iot_class": "local_push", "name": "Trend" }, - "tts": { - "config_flow": false, - "iot_class": null, - "name": "Text-to-Speech (TTS)" - }, "tuya": { "config_flow": true, "iot_class": "cloud_push", @@ -4903,10 +4605,6 @@ "iot_class": "cloud_polling", "name": "UpCloud" }, - "update": { - "config_flow": false, - "iot_class": null - }, "upnp": { "config_flow": true, "iot_class": "local_polling", @@ -4921,11 +4619,6 @@ "iot_class": "cloud_polling", "name": "UptimeRobot" }, - "usb": { - "config_flow": false, - "iot_class": "local_push", - "name": "USB Discovery" - }, "usgs_earthquakes_feed": { "config_flow": false, "iot_class": "cloud_polling", @@ -4936,10 +4629,6 @@ "iot_class": "local_polling", "name": "Ubiquiti UniFi Video" }, - "vacuum": { - "config_flow": false, - "iot_class": null - }, "vallox": { "config_flow": true, "iot_class": "local_polling", @@ -5080,11 +4769,6 @@ "iot_class": "cloud_polling", "name": "World Air Quality Index (WAQI)" }, - "water_heater": { - "config_flow": false, - "iot_class": null, - "name": "Water Heater" - }, "waterfurnace": { "config_flow": false, "iot_class": "cloud_polling", @@ -5099,21 +4783,11 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "weather": { - "config_flow": false, - "iot_class": null, - "name": "Weather" - }, "webhook": { "config_flow": false, "iot_class": null, "name": "Webhook" }, - "websocket_api": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant WebSocket API" - }, "wemo": { "config_flow": true, "iot_class": "local_push", @@ -5334,11 +5008,6 @@ "iot_class": "local_polling", "name": "Zengge" }, - "zeroconf": { - "config_flow": false, - "iot_class": "local_push", - "name": "Zero-configuration networking (zeroconf)" - }, "zerproc": { "config_flow": true, "iot_class": "local_polling", @@ -5486,35 +5155,18 @@ } }, "translated_name": [ - "alarm_control_panel", - "application_credentials", "aurora", - "automation", - "binary_sensor", - "button", - "calendar", - "camera", "cert_expiry", - "climate", - "configurator", - "conversation", - "cover", "cpuspeed", "demo", "derivative", - "device_tracker", - "diagnostics", "emulated_roku", - "energy", - "fan", "filesize", "garages_amsterdam", "google_travel_time", "group", "growatt_server", "homekit_controller", - "humidifier", - "image_processing", "input_boolean", "input_datetime", "input_number", @@ -5522,41 +5174,24 @@ "input_text", "integration", "islamic_prayer_times", - "light", "local_ip", - "lock", - "mailbox", - "media_player", "min_max", "mobile_app", "moehlenhoff_alpha2", "moon", "nmap_tracker", - "notify", - "number", - "person", "plant", "proximity", - "remote", "rpi_power", - "scene", "schedule", - "script", - "select", - "sensor", "shopping_list", - "siren", "sun", - "switch", "switch_as_x", - "system_health", "tag", "threshold", "tod", - "update", "uptime", "utility_meter", - "vacuum", "waze_travel_time" ] } From 1efa374c4ec9371a9888fcc0e6a65ee208e94cf5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Oct 2022 11:45:40 -0400 Subject: [PATCH 101/183] Add a couple more brands (#79600) --- homeassistant/brands/inovelli.json | 5 +++++ homeassistant/brands/jasco.json | 5 +++++ homeassistant/brands/u_tec.json | 5 +++++ homeassistant/brands/zooz.json | 5 +++++ homeassistant/generated/integrations.json | 25 +++++++++++++++++++++++ 5 files changed, 45 insertions(+) create mode 100644 homeassistant/brands/inovelli.json create mode 100644 homeassistant/brands/jasco.json create mode 100644 homeassistant/brands/u_tec.json create mode 100644 homeassistant/brands/zooz.json diff --git a/homeassistant/brands/inovelli.json b/homeassistant/brands/inovelli.json new file mode 100644 index 00000000000..3667a6519c6 --- /dev/null +++ b/homeassistant/brands/inovelli.json @@ -0,0 +1,5 @@ +{ + "domain": "inovelli", + "name": "Inovelli", + "iot_standards": ["zigbee", "zwave"] +} diff --git a/homeassistant/brands/jasco.json b/homeassistant/brands/jasco.json new file mode 100644 index 00000000000..e293b81f994 --- /dev/null +++ b/homeassistant/brands/jasco.json @@ -0,0 +1,5 @@ +{ + "domain": "jasco", + "name": "Jasco", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/brands/u_tec.json b/homeassistant/brands/u_tec.json new file mode 100644 index 00000000000..2ce4be9a7d9 --- /dev/null +++ b/homeassistant/brands/u_tec.json @@ -0,0 +1,5 @@ +{ + "domain": "u_tec", + "name": "U-tec", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/brands/zooz.json b/homeassistant/brands/zooz.json new file mode 100644 index 00000000000..f3032e58653 --- /dev/null +++ b/homeassistant/brands/zooz.json @@ -0,0 +1,5 @@ +{ + "domain": "zooz", + "name": "Zooz", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0fc1f4c2f01..cccbd162e57 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1935,6 +1935,13 @@ "iot_class": "local_push", "name": "INKBIRD" }, + "inovelli": { + "name": "Inovelli", + "iot_standards": [ + "zigbee", + "zwave" + ] + }, "insteon": { "config_flow": true, "iot_class": "local_push", @@ -2009,6 +2016,12 @@ "iot_class": "local_polling", "name": "iZone" }, + "jasco": { + "name": "Jasco", + "iot_standards": [ + "zwave" + ] + }, "jellyfin": { "config_flow": true, "iot_class": "local_polling", @@ -4550,6 +4563,12 @@ "iot_class": "cloud_push", "name": "Twitter" }, + "u_tec": { + "name": "U-tec", + "iot_standards": [ + "zwave" + ] + }, "ubiquiti": { "name": "Ubiquiti", "integrations": { @@ -5048,6 +5067,12 @@ "iot_class": "local_polling", "name": "ZoneMinder" }, + "zooz": { + "name": "Zooz", + "iot_standards": [ + "zwave" + ] + }, "zwave_js": { "config_flow": true, "iot_class": "local_push", From f08fab9d7e3b276fc186bf2821e57e1f0a7682bc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Oct 2022 17:51:12 +0200 Subject: [PATCH 102/183] Update frontend to 20221004.0 (#79602) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 62bed3777a8..61fc1629793 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221003.0"], + "requirements": ["home-assistant-frontend==20221004.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e3dd8b504a5..099207c3a7a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.23.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221003.0 +home-assistant-frontend==20221004.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index c0ed200b1d1..72ec6e0fac1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,7 +865,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221003.0 +home-assistant-frontend==20221004.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a333ff8299..fe99cee9969 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -645,7 +645,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221003.0 +home-assistant-frontend==20221004.0 # homeassistant.components.home_connect homeconnect==0.7.2 From af5f5542d2b4f5d6a06ae6218b74afcbbc8aaf8c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Oct 2022 11:52:10 -0400 Subject: [PATCH 103/183] Bumped version to 2022.10.0b6 --- 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 7c78cf46b96..91fa4d628de 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 = 10 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __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 fdce778845b..22535c696d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0b5" +version = "2022.10.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9302071527d95bfeed212e4f4fe9fa8c04158ce8 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 5 Oct 2022 08:58:24 +0200 Subject: [PATCH 104/183] Netatmo add supported brands (#79563) --- homeassistant/components/netatmo/manifest.json | 8 +++++++- homeassistant/generated/supported_brands.py | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 4095762c666..74d34056241 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -11,5 +11,11 @@ "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] }, "iot_class": "cloud_polling", - "loggers": ["pyatmo"] + "loggers": ["pyatmo"], + "supported_brands": { + "legrand": "Legrand", + "bubendorff": "Bubendorff", + "smarther": "Smarther", + "bticino": "BTicino" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 50490d2c847..5c641c0f95e 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -8,6 +8,7 @@ HAS_SUPPORTED_BRANDS = [ "hunterdouglas_powerview", "inkbird", "motion_blinds", + "netatmo", "overkiz", "renault", "thermobeacon", From 98567b6f2617a36b2b0e54c022f10d784c1c1578 Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Wed, 5 Oct 2022 10:25:46 +0300 Subject: [PATCH 105/183] Add supported brands for switchbee (#79595) --- homeassistant/components/switchbee/manifest.json | 5 ++++- homeassistant/generated/supported_brands.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbee/manifest.json b/homeassistant/components/switchbee/manifest.json index 75e5b2e9bfd..5ca066e3bc0 100644 --- a/homeassistant/components/switchbee/manifest.json +++ b/homeassistant/components/switchbee/manifest.json @@ -5,5 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/switchbee", "requirements": ["pyswitchbee==1.5.5"], "codeowners": ["@jafar-atili"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "supported_brands": { + "bswitch": "BSwitch" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 5c641c0f95e..15f2a580a29 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -11,6 +11,7 @@ HAS_SUPPORTED_BRANDS = [ "netatmo", "overkiz", "renault", + "switchbee", "thermobeacon", "wemo", "yalexs_ble", From 2c34190d8218e7e088a0d82ff5ae6119f345b373 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Oct 2022 08:55:28 -1000 Subject: [PATCH 106/183] Bump dbus-fast to 1.24.0 (#79608) --- 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 3b6f5977157..f81e1324da4 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.23.0" + "dbus-fast==1.24.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 099207c3a7a..afda4684b34 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.23.0 +dbus-fast==1.24.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 72ec6e0fac1..a605d596c3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.23.0 +dbus-fast==1.24.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe99cee9969..9f780813913 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.23.0 +dbus-fast==1.24.0 # homeassistant.components.debugpy debugpy==1.6.3 From ed7c93240c34e0ec458e48ecbab2f688f7b1d9f1 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 4 Oct 2022 14:43:57 -0400 Subject: [PATCH 107/183] Handle state is None in InfluxDB (#79609) --- homeassistant/components/influxdb/__init__.py | 2 +- tests/components/influxdb/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 72871c75fc4..4fd6eb58fdd 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -218,7 +218,7 @@ def _generate_event_to_json(conf: dict) -> Callable[[Event], dict[str, Any] | No state: State | None = event.data.get(EVENT_NEW_STATE) if ( state is None - or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) + or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE, None) or not entity_filter(state.entity_id) ): return None diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 27b9ac82ade..78648852803 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -557,7 +557,7 @@ async def test_event_listener_states( """Test the event listener against ignored states.""" handler_method = await _setup(hass, mock_client, config_ext, get_write_api) - for state_state in (1, "unknown", "", "unavailable"): + for state_state in (1, "unknown", "", "unavailable", None): state = MagicMock( state=state_state, domain="fake", From 895facdf7210707e9fb8c37b1682240638fac97f Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Wed, 5 Oct 2022 02:27:56 -0400 Subject: [PATCH 108/183] Supervisor update entity auto update from api (#79611) * Supervisor update entity auto update from api * Update api mocks in tests --- homeassistant/components/hassio/update.py | 6 +++++- tests/components/hassio/test_binary_sensor.py | 1 + tests/components/hassio/test_init.py | 9 ++++++++- tests/components/hassio/test_sensor.py | 1 + tests/components/hassio/test_update.py | 1 + 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index e68dbece5b6..dcb2b18cdd3 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -219,7 +219,6 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): """Update entity to handle updates for the Home Assistant Supervisor.""" - _attr_auto_update = True _attr_supported_features = UpdateEntityFeature.INSTALL _attr_title = "Home Assistant Supervisor" @@ -233,6 +232,11 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): """Return native value of entity.""" return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION] + @property + def auto_update(self) -> bool: + """Return true if auto-update is enabled for supervisor.""" + return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_AUTO_UPDATE] + @property def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index 31667efadc6..a601f98f1c5 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -75,6 +75,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test", diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 41b679e448a..f0f94661d50 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -92,7 +92,11 @@ def mock_all(aioclient_mock, request, os_info): "http://127.0.0.1/supervisor/info", json={ "result": "ok", - "data": {"version_latest": "1.0.0", "version": "1.0.0"}, + "data": { + "version_latest": "1.0.0", + "version": "1.0.0", + "auto_update": True, + }, "addons": [ { "name": "test", @@ -536,6 +540,7 @@ async def test_device_registry_calls(hass): supervisor_mock_data = { "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test", @@ -586,6 +591,7 @@ async def test_device_registry_calls(hass): supervisor_mock_data = { "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test2", @@ -620,6 +626,7 @@ async def test_device_registry_calls(hass): supervisor_mock_data = { "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test2", diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 868448cec2d..16cce09b800 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -68,6 +68,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test", diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 48f6d894de0..aaa77cde129 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -79,6 +79,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "version": "1.0.0", "version_latest": "1.0.1dev222", + "auto_update": True, "addons": [ { "name": "test", From b4cb70cdebcf73bddbc8ad298887e5fb66ca64b4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 5 Oct 2022 08:20:37 +0200 Subject: [PATCH 109/183] Bump UniFi dependency to v37 (#79617) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 0ff781418d4..6bf9f8aa473 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==36"], + "requirements": ["aiounifi==37"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index a605d596c3b..74318fda9c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==36 +aiounifi==37 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f780813913..67c34f90762 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -251,7 +251,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==36 +aiounifi==37 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From e26f3de80b9d93f6faca98fda9fa9f46796207b3 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 4 Oct 2022 17:17:48 -0400 Subject: [PATCH 110/183] Bump ZHA dependencies (#79623) --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 322f93e8373..803a7daabbe 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.34.0", + "bellows==0.34.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.81", + "zha-quirks==0.0.82", "zigpy-deconz==0.19.0", - "zigpy==0.51.1", + "zigpy==0.51.2", "zigpy-xbee==0.16.0", "zigpy-zigate==0.10.0", "zigpy-znp==0.9.0" diff --git a/requirements_all.txt b/requirements_all.txt index 74318fda9c4..448ab3f2031 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -401,7 +401,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.0 +bellows==0.34.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -2592,7 +2592,7 @@ zengge==0.2 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.81 +zha-quirks==0.0.82 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2613,7 +2613,7 @@ zigpy-zigate==0.10.0 zigpy-znp==0.9.0 # homeassistant.components.zha -zigpy==0.51.1 +zigpy==0.51.2 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67c34f90762..10e996d18b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.0 +bellows==0.34.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -1793,7 +1793,7 @@ youless-api==0.16 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.81 +zha-quirks==0.0.82 # homeassistant.components.zha zigpy-deconz==0.19.0 @@ -1808,7 +1808,7 @@ zigpy-zigate==0.10.0 zigpy-znp==0.9.0 # homeassistant.components.zha -zigpy==0.51.1 +zigpy==0.51.2 # homeassistant.components.zwave_js zwave-js-server-python==0.43.0 From 20c61af5b75def3416978c6a454ab288f497cf7c Mon Sep 17 00:00:00 2001 From: kpine Date: Tue, 4 Oct 2022 23:30:34 -0700 Subject: [PATCH 111/183] Allow picking multiple entity targets for zwave_js.refresh_value service (#79634) Allow selection of multiple entities for zwave_js.refresh_value service --- homeassistant/components/zwave_js/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index eccd46745a3..687d486888c 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -105,6 +105,7 @@ refresh_value: selector: entity: integration: zwave_js + multiple: true refresh_all_values: name: Refresh all values? description: Whether to refresh all values (true) or just the primary value (false) From 146520b437690d34e3adf88f5890e1309039577b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 Oct 2022 09:59:18 +0200 Subject: [PATCH 112/183] Fix search throwing on templated services (#79637) --- homeassistant/const.py | 1 + homeassistant/helpers/config_validation.py | 8 +++++-- homeassistant/helpers/script.py | 14 +++++------ homeassistant/helpers/service.py | 2 +- tests/components/search/test_init.py | 28 ++++++++++++++++++++++ 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 91fa4d628de..801f456d59c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -227,6 +227,7 @@ CONF_SENSOR_TYPE: Final = "sensor_type" CONF_SEQUENCE: Final = "sequence" CONF_SERVICE: Final = "service" CONF_SERVICE_DATA: Final = "data" +CONF_SERVICE_DATA_TEMPLATE: Final = "data_template" CONF_SERVICE_TEMPLATE: Final = "service_template" CONF_SHOW_ON_MAP: Final = "show_on_map" CONF_SLAVE: Final = "slave" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4c2fed60bb4..f6e77ef0018 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -63,6 +63,8 @@ from homeassistant.const import ( CONF_SCENE, CONF_SEQUENCE, CONF_SERVICE, + CONF_SERVICE_DATA, + CONF_SERVICE_DATA_TEMPLATE, CONF_SERVICE_TEMPLATE, CONF_STATE, CONF_STOP, @@ -1119,8 +1121,10 @@ SERVICE_SCHEMA = vol.All( vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): vol.Any( service, dynamic_template ), - vol.Optional("data"): vol.Any(template, vol.All(dict, template_complex)), - vol.Optional("data_template"): vol.Any( + vol.Optional(CONF_SERVICE_DATA): vol.Any( + template, vol.All(dict, template_complex) + ), + vol.Optional(CONF_SERVICE_DATA_TEMPLATE): vol.Any( template, vol.All(dict, template_complex) ), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index e472934fc76..5fc0fdc4706 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -50,6 +50,7 @@ from homeassistant.const import ( CONF_SEQUENCE, CONF_SERVICE, CONF_SERVICE_DATA, + CONF_SERVICE_DATA_TEMPLATE, CONF_STOP, CONF_TARGET, CONF_THEN, @@ -1112,11 +1113,10 @@ async def _async_stop_scripts_at_shutdown(hass, event): _VarsType = Union[dict[str, Any], MappingProxyType] -def _referenced_extract_ids( - data: dict[str, Any] | None, key: str, found: set[str] -) -> None: +def _referenced_extract_ids(data: Any, key: str, found: set[str]) -> None: """Extract referenced IDs.""" - if not data: + # Data may not exist, or be a template + if not isinstance(data, dict): return item_ids = data.get(key) @@ -1300,7 +1300,7 @@ class Script: for data in ( step.get(CONF_TARGET), step.get(CONF_SERVICE_DATA), - step.get(service.CONF_SERVICE_DATA_TEMPLATE), + step.get(CONF_SERVICE_DATA_TEMPLATE), ): _referenced_extract_ids(data, ATTR_AREA_ID, referenced) @@ -1340,7 +1340,7 @@ class Script: for data in ( step.get(CONF_TARGET), step.get(CONF_SERVICE_DATA), - step.get(service.CONF_SERVICE_DATA_TEMPLATE), + step.get(CONF_SERVICE_DATA_TEMPLATE), ): _referenced_extract_ids(data, ATTR_DEVICE_ID, referenced) @@ -1391,7 +1391,7 @@ class Script: step, step.get(CONF_TARGET), step.get(CONF_SERVICE_DATA), - step.get(service.CONF_SERVICE_DATA_TEMPLATE), + step.get(CONF_SERVICE_DATA_TEMPLATE), ): _referenced_extract_ids(data, ATTR_ENTITY_ID, referenced) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 7675686844c..138fa739794 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_SERVICE, CONF_SERVICE_DATA, + CONF_SERVICE_DATA_TEMPLATE, CONF_SERVICE_TEMPLATE, CONF_TARGET, ENTITY_MATCH_ALL, @@ -52,7 +53,6 @@ if TYPE_CHECKING: CONF_SERVICE_ENTITY_ID = "entity_id" -CONF_SERVICE_DATA_TEMPLATE = "data_template" _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index a728ef9b8c4..cc04680d8a4 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -183,6 +183,22 @@ async def test_search(hass): }, ] }, + "script_with_templated_services": { + "sequence": [ + { + "service": "test.script", + "target": "{{ {'entity_id':'test.test1'} }}", + }, + { + "service": "test.script", + "data": "{{ {'entity_id':'test.test2'} }}", + }, + { + "service": "test.script", + "data_template": "{{ {'entity_id':'test.test3'} }}", + }, + ] + }, } }, ) @@ -304,6 +320,18 @@ async def test_search(hass): searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) assert searcher.async_search(search_type, search_id) == {} + # Test search of templated script. We can't find referenced areas, devices or + # entities within templated services, but searching them should not raise or + # otherwise fail. + assert hass.states.get("script.script_with_templated_services") + for search_type, search_id in ( + ("area", "script.script_with_templated_services"), + ("device", "script.script_with_templated_services"), + ("entity", "script.script_with_templated_services"), + ): + searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) + assert searcher.async_search(search_type, search_id) == {} + searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) assert searcher.async_search("entity", "light.wled_config_entry_source") == { "config_entry": {wled_config_entry.entry_id}, From 20db8138d99dc94bf9ed3e4b77b4289f2f1e838b Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Wed, 5 Oct 2022 14:19:03 +0300 Subject: [PATCH 113/183] Enhanced switchbee device naming (#79641) --- homeassistant/components/switchbee/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switchbee/entity.py b/homeassistant/components/switchbee/entity.py index 516932d6f4e..af2d834fd2f 100644 --- a/homeassistant/components/switchbee/entity.py +++ b/homeassistant/components/switchbee/entity.py @@ -47,7 +47,7 @@ class SwitchBeeDeviceEntity(SwitchBeeEntity[_DeviceTypeT]): super().__init__(device, coordinator) self._is_online: bool = True self._attr_device_info = DeviceInfo( - name=f"SwitchBee {device.unit_id}", + name=device.zone, identifiers={ ( DOMAIN, From 7e579331641fddb3c33f1d4466812f90ced21b83 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 Oct 2022 16:27:08 +0200 Subject: [PATCH 114/183] Normalize to kWh when handling WS energy/fossil_energy_consumption (#79649) * Normalize to kWh when handling WS energy/fossil_energy_consumption * Improve test --- homeassistant/components/energy/websocket_api.py | 2 ++ tests/components/energy/test_websocket_api.py | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index ad77308b410..7ba83cf15c9 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -13,6 +13,7 @@ from typing import Any, cast import voluptuous as vol from homeassistant.components import recorder, websocket_api +from homeassistant.const import ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, @@ -268,6 +269,7 @@ async def ws_get_fossil_energy_consumption( statistic_ids, "hour", True, + {"energy": ENERGY_KILO_WATT_HOUR}, ) def _combine_sum_statistics( diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index ab785291f91..343e814f3a8 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -814,25 +814,25 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "start": period1, "last_reset": None, "state": 0, - "sum": 20, + "sum": 20000, }, { "start": period2, "last_reset": None, "state": 1, - "sum": 30, + "sum": 30000, }, { "start": period3, "last_reset": None, "state": 2, - "sum": 40, + "sum": 40000, }, { "start": period4, "last_reset": None, "state": 3, - "sum": 50, + "sum": 50000, }, ) external_energy_metadata_2 = { @@ -841,7 +841,7 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "name": "Total imported energy", "source": "test", "statistic_id": "test:total_energy_import_tariff_2", - "unit_of_measurement": "kWh", + "unit_of_measurement": "Wh", } external_co2_statistics = ( { From 99386428001a1437a9f6ce89f404c33bfd8cd839 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Oct 2022 17:38:32 +0200 Subject: [PATCH 115/183] Update frontend to 20221005.0 (#79656) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 61fc1629793..e6d5f63272d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221004.0"], + "requirements": ["home-assistant-frontend==20221005.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index afda4684b34..2f637ba61f1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.24.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221004.0 +home-assistant-frontend==20221005.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 448ab3f2031..1359a9c1b45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,7 +865,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221004.0 +home-assistant-frontend==20221005.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10e996d18b7..ba5ca682243 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -645,7 +645,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221004.0 +home-assistant-frontend==20221005.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 7a1757a61fb5f1114c4db589b607c19a67025446 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 Oct 2022 18:05:12 +0200 Subject: [PATCH 116/183] Bumped version to 2022.10.0 --- 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 801f456d59c..139b3a157b2 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 = 10 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __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 22535c696d6..8152dd57cfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0b6" +version = "2022.10.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9f7f13ac872f309443f3473d3381b120af86ce25 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Oct 2022 21:42:44 +0200 Subject: [PATCH 117/183] Don't normalize units of long term statistics (#79320) * Don't normalize units of long term statistics * Update statistics.py --- .../components/recorder/statistics.py | 37 +-- homeassistant/components/sensor/recorder.py | 52 ++-- .../components/recorder/test_websocket_api.py | 54 ++-- tests/components/sensor/test_recorder.py | 233 ++++++------------ 4 files changed, 155 insertions(+), 221 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 5ecf1dc1e44..7ba5c5f8c73 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -126,14 +126,14 @@ QUERY_STATISTIC_META = [ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { - DistanceConverter.NORMALIZED_UNIT: DistanceConverter, - EnergyConverter.NORMALIZED_UNIT: EnergyConverter, - MassConverter.NORMALIZED_UNIT: MassConverter, - PowerConverter.NORMALIZED_UNIT: PowerConverter, - PressureConverter.NORMALIZED_UNIT: PressureConverter, - SpeedConverter.NORMALIZED_UNIT: SpeedConverter, - TemperatureConverter.NORMALIZED_UNIT: TemperatureConverter, - VolumeConverter.NORMALIZED_UNIT: VolumeConverter, + **{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS}, + **{unit: EnergyConverter for unit in EnergyConverter.VALID_UNITS}, + **{unit: MassConverter for unit in MassConverter.VALID_UNITS}, + **{unit: PowerConverter for unit in PowerConverter.VALID_UNITS}, + **{unit: PressureConverter for unit in PressureConverter.VALID_UNITS}, + **{unit: SpeedConverter for unit in SpeedConverter.VALID_UNITS}, + **{unit: TemperatureConverter for unit in TemperatureConverter.VALID_UNITS}, + **{unit: VolumeConverter for unit in VolumeConverter.VALID_UNITS}, } @@ -141,7 +141,7 @@ _LOGGER = logging.getLogger(__name__) def _get_unit_class(unit: str | None) -> str | None: - """Get corresponding unit class from from the normalized statistics unit.""" + """Get corresponding unit class from from the statistics unit.""" if converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(unit): return converter.UNIT_CLASS return None @@ -152,7 +152,7 @@ def _get_statistic_to_display_unit_converter( state_unit: str | None, requested_units: dict[str, str] | None, ) -> Callable[[float | None], float | None]: - """Prepare a converter from the normalized statistics unit to display unit.""" + """Prepare a converter from the statistics unit to display unit.""" def no_conversion(val: float | None) -> float | None: """Return val.""" @@ -176,21 +176,26 @@ def _get_statistic_to_display_unit_converter( return no_conversion def from_normalized_unit( - val: float | None, conv: type[BaseUnitConverter], to_unit: str + val: float | None, conv: type[BaseUnitConverter], from_unit: str, to_unit: str ) -> float | None: """Return val.""" if val is None: return val - return conv.convert(val, from_unit=conv.NORMALIZED_UNIT, to_unit=to_unit) + return conv.convert(val, from_unit=from_unit, to_unit=to_unit) - return partial(from_normalized_unit, conv=converter, to_unit=display_unit) + return partial( + from_normalized_unit, + conv=converter, + from_unit=statistic_unit, + to_unit=display_unit, + ) def _get_display_to_statistic_unit_converter( display_unit: str | None, statistic_unit: str | None, ) -> Callable[[float], float]: - """Prepare a converter from the display unit to the normalized statistics unit.""" + """Prepare a converter from the display unit to the statistics unit.""" def no_conversion(val: float) -> float: """Return val.""" @@ -202,9 +207,7 @@ def _get_display_to_statistic_unit_converter( if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return no_conversion - return partial( - converter.convert, from_unit=display_unit, to_unit=converter.NORMALIZED_UNIT - ) + return partial(converter.convert, from_unit=display_unit, to_unit=statistic_unit) def _get_unit_converter( diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 4380efbd2c3..144502dd81a 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -164,10 +164,10 @@ def _normalize_states( if device_class not in UNIT_CONVERTERS or ( old_metadata and old_metadata["unit_of_measurement"] - != UNIT_CONVERTERS[device_class].NORMALIZED_UNIT + not in UNIT_CONVERTERS[device_class].VALID_UNITS ): # We're either not normalizing this device class or this entity is not stored - # normalized, return the states as they are + # in a supported unit, return the states as they are fstates = [] for state in entity_history: try: @@ -205,6 +205,10 @@ def _normalize_states( converter = UNIT_CONVERTERS[device_class] fstates = [] + statistics_unit: str | None = None + if old_metadata: + statistics_unit = old_metadata["unit_of_measurement"] + for state in entity_history: try: fstate = _parse_float(state.state) @@ -224,17 +228,19 @@ def _normalize_states( device_class, ) continue + if statistics_unit is None: + statistics_unit = state_unit fstates.append( ( converter.convert( - fstate, from_unit=state_unit, to_unit=converter.NORMALIZED_UNIT + fstate, from_unit=state_unit, to_unit=statistics_unit ), state, ) ) - return UNIT_CONVERTERS[device_class].NORMALIZED_UNIT, state_unit, fstates + return statistics_unit, state_unit, fstates def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: @@ -423,7 +429,7 @@ def _compile_statistics( # noqa: C901 device_class = _state.attributes.get(ATTR_DEVICE_CLASS) entity_history = history_list[entity_id] - normalized_unit, state_unit, fstates = _normalize_states( + statistics_unit, state_unit, fstates = _normalize_states( hass, session, old_metadatas, @@ -438,7 +444,7 @@ def _compile_statistics( # noqa: C901 state_class = _state.attributes[ATTR_STATE_CLASS] to_process.append( - (entity_id, normalized_unit, state_unit, state_class, fstates) + (entity_id, statistics_unit, state_unit, state_class, fstates) ) if "sum" in wanted_statistics[entity_id]: to_query.append(entity_id) @@ -448,14 +454,14 @@ def _compile_statistics( # noqa: C901 ) for ( # pylint: disable=too-many-nested-blocks entity_id, - normalized_unit, + statistics_unit, state_unit, state_class, fstates, ) in to_process: # Check metadata if old_metadata := old_metadatas.get(entity_id): - if old_metadata[1]["unit_of_measurement"] != normalized_unit: + if old_metadata[1]["unit_of_measurement"] != statistics_unit: if WARN_UNSTABLE_UNIT not in hass.data: hass.data[WARN_UNSTABLE_UNIT] = set() if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: @@ -467,7 +473,7 @@ def _compile_statistics( # noqa: C901 "Go to %s to fix this", "normalized " if device_class in UNIT_CONVERTERS else "", entity_id, - normalized_unit, + statistics_unit, old_metadata[1]["unit_of_measurement"], old_metadata[1]["unit_of_measurement"], LINK_DEV_STATISTICS, @@ -481,7 +487,7 @@ def _compile_statistics( # noqa: C901 "name": None, "source": RECORDER_DOMAIN, "statistic_id": entity_id, - "unit_of_measurement": normalized_unit, + "unit_of_measurement": statistics_unit, } # Make calculations @@ -629,14 +635,13 @@ def list_statistic_ids( if state_unit not in converter.VALID_UNITS: continue - statistics_unit = converter.NORMALIZED_UNIT result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, "name": None, "source": RECORDER_DOMAIN, "statistic_id": state.entity_id, - "unit_of_measurement": statistics_unit, + "unit_of_measurement": state_unit, } return result @@ -680,13 +685,13 @@ def validate_statistics( metadata_unit = metadata[1]["unit_of_measurement"] if device_class not in UNIT_CONVERTERS: - issue_type = ( - "units_changed_can_convert" - if statistics.can_convert_units(metadata_unit, state_unit) - else "units_changed" - ) if state_unit != metadata_unit: # The unit has changed + issue_type = ( + "units_changed_can_convert" + if statistics.can_convert_units(metadata_unit, state_unit) + else "units_changed" + ) validation_result[entity_id].append( statistics.ValidationIssue( issue_type, @@ -697,22 +702,19 @@ def validate_statistics( }, ) ) - elif metadata_unit != UNIT_CONVERTERS[device_class].NORMALIZED_UNIT: + elif metadata_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS: # The unit in metadata is not supported for this device class - statistics_unit = UNIT_CONVERTERS[device_class].NORMALIZED_UNIT - issue_type = ( - "unsupported_unit_metadata_can_convert" - if statistics.can_convert_units(metadata_unit, statistics_unit) - else "unsupported_unit_metadata" + valid_units = ", ".join( + sorted(UNIT_CONVERTERS[device_class].VALID_UNITS) ) validation_result[entity_id].append( statistics.ValidationIssue( - issue_type, + "unsupported_unit_metadata", { "statistic_id": entity_id, "device_class": device_class, "metadata_unit": metadata_unit, - "supported_unit": statistics_unit, + "supported_unit": valid_units, }, ) ) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 58893ee3bb1..6058fd6a2e5 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -591,26 +591,26 @@ async def test_statistics_during_period_bad_end_time( [ (IMPERIAL_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"), (METRIC_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"), - (IMPERIAL_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "m", "distance"), - (METRIC_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "m", "distance"), - (IMPERIAL_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), - (METRIC_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), - (IMPERIAL_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (METRIC_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), - (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), - (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), - (IMPERIAL_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "m/s", "speed"), - (METRIC_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "m/s", "speed"), + (IMPERIAL_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "ft", "distance"), + (METRIC_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "ft", "distance"), + (IMPERIAL_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "Wh", "energy"), + (METRIC_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "Wh", "energy"), + (IMPERIAL_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (METRIC_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "kW", "power"), + (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "kW", "power"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "hPa", "pressure"), + (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "hPa", "pressure"), + (IMPERIAL_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "km/h", "speed"), + (METRIC_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "km/h", "speed"), (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), - (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "m³", "volume"), - (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "m³", "volume"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°F", "temperature"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°F", "temperature"), + (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "ft³", "volume"), + (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "ft³", "volume"), ], ) async def test_list_statistic_ids( @@ -904,7 +904,7 @@ async def test_update_statistics_metadata( "name": None, "source": "recorder", "statistics_unit_of_measurement": "kW", - "unit_class": None, + "unit_class": "power", } ] @@ -994,7 +994,7 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): "name": None, "source": "recorder", "statistics_unit_of_measurement": "kW", - "unit_class": None, + "unit_class": "power", } ] @@ -1101,7 +1101,7 @@ async def test_change_statistics_unit_errors( "name": None, "source": "recorder", "statistics_unit_of_measurement": "kW", - "unit_class": None, + "unit_class": "power", } ] @@ -1504,7 +1504,7 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "statistics_unit_of_measurement": unit, + "statistics_unit_of_measurement": attributes["unit_of_measurement"], "unit_class": unit_class, } ] @@ -1531,7 +1531,7 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "statistics_unit_of_measurement": unit, + "statistics_unit_of_measurement": attributes["unit_of_measurement"], "unit_class": unit_class, } ] @@ -2160,9 +2160,9 @@ async def test_adjust_sum_statistics_gas( "state_unit, statistic_unit, unit_class, factor, valid_units, invalid_units", ( ("kWh", "kWh", "energy", 1, ("Wh", "kWh", "MWh"), ("ft³", "m³", "cats", None)), - ("MWh", "MWh", None, 1, ("MWh",), ("Wh", "kWh", "ft³", "m³", "cats", None)), + ("MWh", "MWh", "energy", 1, ("Wh", "kWh", "MWh"), ("ft³", "m³", "cats", None)), ("m³", "m³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), - ("ft³", "ft³", None, 1, ("ft³",), ("m³", "Wh", "kWh", "MWh", "cats", None)), + ("ft³", "ft³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), ("dogs", "dogs", None, 1, ("dogs",), ("cats", None)), (None, None, None, 1, (None,), ("cats",)), ), @@ -2262,7 +2262,7 @@ async def test_adjust_sum_statistics_errors( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "statistics_unit_of_measurement": statistic_unit, + "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, } ] @@ -2276,7 +2276,7 @@ async def test_adjust_sum_statistics_errors( "name": "Total imported energy", "source": source, "statistic_id": statistic_id, - "unit_of_measurement": statistic_unit, + "unit_of_measurement": state_unit, }, ) } diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 637d17e21a8..99aa3a3bf8e 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -86,22 +86,22 @@ def set_time_zone(): ("battery", "%", "%", "%", None, 13.050847, -10, 30), ("battery", None, None, None, None, 13.050847, -10, 30), ("distance", "m", "m", "m", "distance", 13.050847, -10, 30), - ("distance", "mi", "mi", "m", "distance", 13.050847, -10, 30), + ("distance", "mi", "mi", "mi", "distance", 13.050847, -10, 30), ("humidity", "%", "%", "%", None, 13.050847, -10, 30), ("humidity", None, None, None, None, 13.050847, -10, 30), ("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "hPa", "hPa", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "mbar", "mbar", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "inHg", "inHg", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "psi", "psi", "Pa", "pressure", 13.050847, -10, 30), + ("pressure", "hPa", "hPa", "hPa", "pressure", 13.050847, -10, 30), + ("pressure", "mbar", "mbar", "mbar", "pressure", 13.050847, -10, 30), + ("pressure", "inHg", "inHg", "inHg", "pressure", 13.050847, -10, 30), + ("pressure", "psi", "psi", "psi", "pressure", 13.050847, -10, 30), ("speed", "m/s", "m/s", "m/s", "speed", 13.050847, -10, 30), - ("speed", "mph", "mph", "m/s", "speed", 13.050847, -10, 30), + ("speed", "mph", "mph", "mph", "speed", 13.050847, -10, 30), ("temperature", "°C", "°C", "°C", "temperature", 13.050847, -10, 30), - ("temperature", "°F", "°F", "°C", "temperature", 13.050847, -10, 30), + ("temperature", "°F", "°F", "°F", "temperature", 13.050847, -10, 30), ("volume", "m³", "m³", "m³", "volume", 13.050847, -10, 30), - ("volume", "ft³", "ft³", "m³", "volume", 13.050847, -10, 30), + ("volume", "ft³", "ft³", "ft³", "volume", 13.050847, -10, 30), ("weight", "g", "g", "g", "mass", 13.050847, -10, 30), - ("weight", "oz", "oz", "g", "mass", 13.050847, -10, 30), + ("weight", "oz", "oz", "oz", "mass", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics( @@ -355,29 +355,29 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "units, device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ (IMPERIAL_SYSTEM, "distance", "m", "m", "m", "distance", 1), - (IMPERIAL_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), + (IMPERIAL_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1), (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), - (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), + (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "Wh", "energy", 1), (IMPERIAL_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), - (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "ft³", "volume", 1), (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), (IMPERIAL_SYSTEM, "volume", "m³", "m³", "m³", "volume", 1), - (IMPERIAL_SYSTEM, "volume", "ft³", "ft³", "m³", "volume", 1), + (IMPERIAL_SYSTEM, "volume", "ft³", "ft³", "ft³", "volume", 1), (IMPERIAL_SYSTEM, "weight", "g", "g", "g", "mass", 1), - (IMPERIAL_SYSTEM, "weight", "oz", "oz", "g", "mass", 1), + (IMPERIAL_SYSTEM, "weight", "oz", "oz", "oz", "mass", 1), (METRIC_SYSTEM, "distance", "m", "m", "m", "distance", 1), - (METRIC_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), + (METRIC_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1), (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), - (METRIC_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), + (METRIC_SYSTEM, "energy", "Wh", "Wh", "Wh", "energy", 1), (METRIC_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), - (METRIC_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (METRIC_SYSTEM, "gas", "ft³", "ft³", "ft³", "volume", 1), (METRIC_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), (METRIC_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), (METRIC_SYSTEM, "volume", "m³", "m³", "m³", "volume", 1), - (METRIC_SYSTEM, "volume", "ft³", "ft³", "m³", "volume", 1), + (METRIC_SYSTEM, "volume", "ft³", "ft³", "ft³", "volume", 1), (METRIC_SYSTEM, "weight", "g", "g", "g", "mass", 1), - (METRIC_SYSTEM, "weight", "oz", "oz", "g", "mass", 1), + (METRIC_SYSTEM, "weight", "oz", "oz", "oz", "mass", 1), ], ) async def test_compile_hourly_sum_statistics_amount( @@ -548,11 +548,11 @@ async def test_compile_hourly_sum_statistics_amount( "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ ("energy", "kWh", "kWh", "kWh", "energy", 1), - ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "Wh", "energy", 1), ("monetary", "EUR", "EUR", "EUR", None, 1), ("monetary", "SEK", "SEK", "SEK", None, 1), ("gas", "m³", "m³", "m³", "volume", 1), - ("gas", "ft³", "ft³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "ft³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_amount_reset_every_state_change( @@ -957,11 +957,11 @@ def test_compile_hourly_sum_statistics_negative_state( "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ ("energy", "kWh", "kWh", "kWh", "energy", 1), - ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "Wh", "energy", 1), ("monetary", "EUR", "EUR", "EUR", None, 1), ("monetary", "SEK", "SEK", "SEK", None, 1), ("gas", "m³", "m³", "m³", "volume", 1), - ("gas", "ft³", "ft³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "ft³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_total_no_reset( @@ -1061,9 +1061,9 @@ def test_compile_hourly_sum_statistics_total_no_reset( "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ ("energy", "kWh", "kWh", "kWh", "energy", 1), - ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "Wh", "energy", 1), ("gas", "m³", "m³", "m³", "volume", 1), - ("gas", "ft³", "ft³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "ft³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_total_increasing( @@ -1431,7 +1431,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "statistics_unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "Wh", "unit_class": "energy", }, ] @@ -1728,40 +1728,40 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("measurement", "battery", "%", "%", "%", None, "mean"), ("measurement", "battery", None, None, None, None, "mean"), ("measurement", "distance", "m", "m", "m", "distance", "mean"), - ("measurement", "distance", "mi", "mi", "m", "distance", "mean"), + ("measurement", "distance", "mi", "mi", "mi", "distance", "mean"), ("total", "distance", "m", "m", "m", "distance", "sum"), - ("total", "distance", "mi", "mi", "m", "distance", "sum"), - ("total", "energy", "Wh", "Wh", "kWh", "energy", "sum"), + ("total", "distance", "mi", "mi", "mi", "distance", "sum"), + ("total", "energy", "Wh", "Wh", "Wh", "energy", "sum"), ("total", "energy", "kWh", "kWh", "kWh", "energy", "sum"), - ("measurement", "energy", "Wh", "Wh", "kWh", "energy", "mean"), + ("measurement", "energy", "Wh", "Wh", "Wh", "energy", "mean"), ("measurement", "energy", "kWh", "kWh", "kWh", "energy", "mean"), ("measurement", "humidity", "%", "%", "%", None, "mean"), ("measurement", "humidity", None, None, None, None, "mean"), ("total", "monetary", "USD", "USD", "USD", None, "sum"), ("total", "monetary", "None", "None", "None", None, "sum"), ("total", "gas", "m³", "m³", "m³", "volume", "sum"), - ("total", "gas", "ft³", "ft³", "m³", "volume", "sum"), + ("total", "gas", "ft³", "ft³", "ft³", "volume", "sum"), ("measurement", "monetary", "USD", "USD", "USD", None, "mean"), ("measurement", "monetary", "None", "None", "None", None, "mean"), ("measurement", "gas", "m³", "m³", "m³", "volume", "mean"), - ("measurement", "gas", "ft³", "ft³", "m³", "volume", "mean"), + ("measurement", "gas", "ft³", "ft³", "ft³", "volume", "mean"), ("measurement", "pressure", "Pa", "Pa", "Pa", "pressure", "mean"), - ("measurement", "pressure", "hPa", "hPa", "Pa", "pressure", "mean"), - ("measurement", "pressure", "mbar", "mbar", "Pa", "pressure", "mean"), - ("measurement", "pressure", "inHg", "inHg", "Pa", "pressure", "mean"), - ("measurement", "pressure", "psi", "psi", "Pa", "pressure", "mean"), + ("measurement", "pressure", "hPa", "hPa", "hPa", "pressure", "mean"), + ("measurement", "pressure", "mbar", "mbar", "mbar", "pressure", "mean"), + ("measurement", "pressure", "inHg", "inHg", "inHg", "pressure", "mean"), + ("measurement", "pressure", "psi", "psi", "psi", "pressure", "mean"), ("measurement", "speed", "m/s", "m/s", "m/s", "speed", "mean"), - ("measurement", "speed", "mph", "mph", "m/s", "speed", "mean"), + ("measurement", "speed", "mph", "mph", "mph", "speed", "mean"), ("measurement", "temperature", "°C", "°C", "°C", "temperature", "mean"), - ("measurement", "temperature", "°F", "°F", "°C", "temperature", "mean"), + ("measurement", "temperature", "°F", "°F", "°F", "temperature", "mean"), ("measurement", "volume", "m³", "m³", "m³", "volume", "mean"), - ("measurement", "volume", "ft³", "ft³", "m³", "volume", "mean"), + ("measurement", "volume", "ft³", "ft³", "ft³", "volume", "mean"), ("total", "volume", "m³", "m³", "m³", "volume", "sum"), - ("total", "volume", "ft³", "ft³", "m³", "volume", "sum"), + ("total", "volume", "ft³", "ft³", "ft³", "volume", "sum"), ("measurement", "weight", "g", "g", "g", "mass", "mean"), - ("measurement", "weight", "oz", "oz", "g", "mass", "mean"), + ("measurement", "weight", "oz", "oz", "oz", "mass", "mean"), ("total", "weight", "g", "g", "g", "mass", "sum"), - ("total", "weight", "oz", "oz", "g", "mass", "sum"), + ("total", "weight", "oz", "oz", "oz", "mass", "sum"), ], ) def test_list_statistic_ids( @@ -2134,7 +2134,7 @@ def test_compile_hourly_statistics_changing_units_3( @pytest.mark.parametrize( "device_class, state_unit, statistic_unit, unit_class, mean1, mean2, min, max", [ - ("power", "kW", "W", None, 13.050847, 13.333333, -10, 30), + ("power", "kW", "kW", "power", 13.050847, 13.333333, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_1( @@ -2207,7 +2207,7 @@ def test_compile_hourly_statistics_changing_device_class_1( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - # Run statistics again, we get a warning, and no additional statistics is generated + # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) @@ -2265,13 +2265,9 @@ def test_compile_hourly_statistics_changing_device_class_1( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - # Run statistics again, we get a warning, and no additional statistics is generated + # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=20)) wait_recording_done(hass) - assert ( - f"The normalized unit of sensor.test1 ({statistic_unit}) does not match the " - f"unit of already compiled statistics ({state_unit})" in caplog.text - ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { @@ -2311,15 +2307,28 @@ def test_compile_hourly_statistics_changing_device_class_1( "state": None, "sum": None, }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(minutes=20) + ), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=25)), + "mean": approx(mean2), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, ] } assert "Error while processing event StatisticsTask" not in caplog.text @pytest.mark.parametrize( - "device_class, state_unit, display_unit, statistic_unit, unit_class, mean, min, max", + "device_class, state_unit, display_unit, statistic_unit, unit_class, mean, mean2, min, max", [ - ("power", "kW", "kW", "W", "power", 13.050847, -10, 30), + ("power", "kW", "kW", "kW", "power", 13.050847, 13.333333, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_2( @@ -2331,6 +2340,7 @@ def test_compile_hourly_statistics_changing_device_class_2( statistic_unit, unit_class, mean, + mean2, min, max, ): @@ -2393,13 +2403,9 @@ def test_compile_hourly_statistics_changing_device_class_2( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - # Run statistics again, we get a warning, and no additional statistics is generated + # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) - assert ( - f"The unit of sensor.test1 ({state_unit}) does not match the " - f"unit of already compiled statistics ({statistic_unit})" in caplog.text - ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { @@ -2425,7 +2431,20 @@ def test_compile_hourly_statistics_changing_device_class_2( "last_reset": None, "state": None, "sum": None, - } + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(minutes=10) + ), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "mean": approx(mean2), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, ] } assert "Error while processing event StatisticsTask" not in caplog.text @@ -3120,13 +3139,13 @@ async def test_validate_statistics_supported_device_class( @pytest.mark.parametrize( - "units, attributes, unit", + "units, attributes, valid_units", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W, kW"), ], ) async def test_validate_statistics_supported_device_class_2( - hass, hass_ws_client, recorder_mock, units, attributes, unit + hass, hass_ws_client, recorder_mock, units, attributes, valid_units ): """Test validate_statistics.""" id = 1 @@ -3172,7 +3191,7 @@ async def test_validate_statistics_supported_device_class_2( "device_class": attributes["device_class"], "metadata_unit": None, "statistic_id": "sensor.test", - "supported_unit": unit, + "supported_unit": valid_units, }, "type": "unsupported_unit_metadata", } @@ -3192,7 +3211,7 @@ async def test_validate_statistics_supported_device_class_2( "device_class": attributes["device_class"], "metadata_unit": None, "statistic_id": "sensor.test", - "supported_unit": unit, + "supported_unit": valid_units, }, "type": "unsupported_unit_metadata", }, @@ -3209,96 +3228,6 @@ async def test_validate_statistics_supported_device_class_2( await assert_validation_result(client, expected) -@pytest.mark.parametrize( - "units, attributes, unit", - [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - ], -) -async def test_validate_statistics_supported_device_class_3( - hass, hass_ws_client, recorder_mock, units, attributes, unit -): - """Test validate_statistics.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - now = dt_util.utcnow() - - hass.config.units = units - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - client = await hass_ws_client() - - # No statistics, no state - empty response - await assert_validation_result(client, {}) - - # No statistics, valid state - empty response - initial_attributes = {"state_class": "measurement", "unit_of_measurement": "kW"} - hass.states.async_set("sensor.test", 10, attributes=initial_attributes) - await hass.async_block_till_done() - await assert_validation_result(client, {}) - - # Statistics has run, device class set - expect error - do_adhoc_statistics(hass, start=now) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", 12, attributes=attributes) - await hass.async_block_till_done() - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "metadata_unit": "kW", - "statistic_id": "sensor.test", - "supported_unit": unit, - }, - "type": "unsupported_unit_metadata_can_convert", - } - ], - } - await assert_validation_result(client, expected) - - # Invalid state too, expect double errors - hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await async_recorder_block_till_done(hass) - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "metadata_unit": "kW", - "statistic_id": "sensor.test", - "supported_unit": unit, - }, - "type": "unsupported_unit_metadata_can_convert", - }, - { - "data": { - "device_class": attributes["device_class"], - "state_unit": "dogs", - "statistic_id": "sensor.test", - }, - "type": "unsupported_unit_state", - }, - ], - } - await assert_validation_result(client, expected) - - @pytest.mark.parametrize( "units, attributes, unit", [ From 92afbf01e7535759b53fc467b31469f1155f96cf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Oct 2022 03:50:05 +0200 Subject: [PATCH 118/183] Simplify long term statistics by always supporting unit conversion (#79557) --- homeassistant/components/sensor/recorder.py | 186 +++------ tests/components/sensor/test_recorder.py | 441 +++++++++++--------- 2 files changed, 308 insertions(+), 319 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 144502dd81a..1a72444c758 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -23,22 +23,11 @@ from homeassistant.components.recorder.models import ( StatisticMetaData, StatisticResult, ) -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources from homeassistant.util import dt as dt_util -from homeassistant.util.unit_conversion import ( - BaseUnitConverter, - DistanceConverter, - EnergyConverter, - MassConverter, - PowerConverter, - PressureConverter, - SpeedConverter, - TemperatureConverter, - VolumeConverter, -) from . import ( ATTR_LAST_RESET, @@ -48,7 +37,6 @@ from . import ( STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, STATE_CLASSES, - SensorDeviceClass, ) _LOGGER = logging.getLogger(__name__) @@ -59,18 +47,6 @@ DEFAULT_STATISTICS = { STATE_CLASS_TOTAL_INCREASING: {"sum"}, } -UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { - SensorDeviceClass.DISTANCE: DistanceConverter, - SensorDeviceClass.ENERGY: EnergyConverter, - SensorDeviceClass.GAS: VolumeConverter, - SensorDeviceClass.POWER: PowerConverter, - SensorDeviceClass.PRESSURE: PressureConverter, - SensorDeviceClass.SPEED: SpeedConverter, - SensorDeviceClass.TEMPERATURE: TemperatureConverter, - SensorDeviceClass.VOLUME: VolumeConverter, - SensorDeviceClass.WEIGHT: MassConverter, -} - # Keep track of entities for which a warning about decreasing value has been logged SEEN_DIP = "sensor_seen_total_increasing_dip" WARN_DIP = "sensor_warn_total_increasing_dip" @@ -154,84 +130,84 @@ def _normalize_states( session: Session, old_metadatas: dict[str, tuple[int, StatisticMetaData]], entity_history: Iterable[State], - device_class: str | None, entity_id: str, ) -> tuple[str | None, str | None, list[tuple[float, State]]]: """Normalize units.""" old_metadata = old_metadatas[entity_id][1] if entity_id in old_metadatas else None state_unit: str | None = None - if device_class not in UNIT_CONVERTERS or ( + fstates: list[tuple[float, State]] = [] + for state in entity_history: + try: + fstate = _parse_float(state.state) + except (ValueError, TypeError): # TypeError to guard for NULL state in DB + continue + fstates.append((fstate, state)) + + if not fstates: + return None, None, fstates + + state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if state_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER or ( old_metadata and old_metadata["unit_of_measurement"] - not in UNIT_CONVERTERS[device_class].VALID_UNITS + not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER ): # We're either not normalizing this device class or this entity is not stored - # in a supported unit, return the states as they are - fstates = [] - for state in entity_history: - try: - fstate = _parse_float(state.state) - except (ValueError, TypeError): # TypeError to guard for NULL state in DB - continue - fstates.append((fstate, state)) + # in a unit which can be converted, return the states as they are - if fstates: - all_units = _get_units(fstates) - if len(all_units) > 1: - if WARN_UNSTABLE_UNIT not in hass.data: - hass.data[WARN_UNSTABLE_UNIT] = set() - if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: - hass.data[WARN_UNSTABLE_UNIT].add(entity_id) - extra = "" - if old_metadata: - extra = ( - " and matches the unit of already compiled statistics " - f"({old_metadata['unit_of_measurement']})" - ) - _LOGGER.warning( - "The unit of %s is changing, got multiple %s, generation of long term " - "statistics will be suppressed unless the unit is stable%s. " - "Go to %s to fix this", - entity_id, - all_units, - extra, - LINK_DEV_STATISTICS, + all_units = _get_units(fstates) + if len(all_units) > 1: + if WARN_UNSTABLE_UNIT not in hass.data: + hass.data[WARN_UNSTABLE_UNIT] = set() + if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: + hass.data[WARN_UNSTABLE_UNIT].add(entity_id) + extra = "" + if old_metadata: + extra = ( + " and matches the unit of already compiled statistics " + f"({old_metadata['unit_of_measurement']})" ) - return None, None, [] - state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + _LOGGER.warning( + "The unit of %s is changing, got multiple %s, generation of long term " + "statistics will be suppressed unless the unit is stable%s. " + "Go to %s to fix this", + entity_id, + all_units, + extra, + LINK_DEV_STATISTICS, + ) + return None, None, [] + state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) return state_unit, state_unit, fstates - converter = UNIT_CONVERTERS[device_class] - fstates = [] + converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[state_unit] + valid_fstates: list[tuple[float, State]] = [] statistics_unit: str | None = None if old_metadata: statistics_unit = old_metadata["unit_of_measurement"] - for state in entity_history: - try: - fstate = _parse_float(state.state) - except ValueError: - continue + for fstate, state in fstates: state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - # Exclude unsupported units from statistics + # Exclude states with unsupported unit from statistics if state_unit not in converter.VALID_UNITS: if WARN_UNSUPPORTED_UNIT not in hass.data: hass.data[WARN_UNSUPPORTED_UNIT] = set() if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]: hass.data[WARN_UNSUPPORTED_UNIT].add(entity_id) _LOGGER.warning( - "%s has unit %s which is unsupported for device_class %s", + "%s has unit %s which can't be converted to %s", entity_id, state_unit, - device_class, + statistics_unit, ) continue if statistics_unit is None: statistics_unit = state_unit - fstates.append( + valid_fstates.append( ( converter.convert( fstate, from_unit=state_unit, to_unit=statistics_unit @@ -240,7 +216,7 @@ def _normalize_states( ) ) - return statistics_unit, state_unit, fstates + return statistics_unit, state_unit, valid_fstates def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: @@ -427,14 +403,12 @@ def _compile_statistics( # noqa: C901 if entity_id not in history_list: continue - device_class = _state.attributes.get(ATTR_DEVICE_CLASS) entity_history = history_list[entity_id] statistics_unit, state_unit, fstates = _normalize_states( hass, session, old_metadatas, entity_history, - device_class, entity_id, ) @@ -467,11 +441,11 @@ def _compile_statistics( # noqa: C901 if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: hass.data[WARN_UNSTABLE_UNIT].add(entity_id) _LOGGER.warning( - "The %sunit of %s (%s) does not match the unit of already " + "The unit of %s (%s) can not be converted to the unit of previously " "compiled statistics (%s). Generation of long term statistics " - "will be suppressed unless the unit changes back to %s. " + "will be suppressed unless the unit changes back to %s or a " + "compatible unit. " "Go to %s to fix this", - "normalized " if device_class in UNIT_CONVERTERS else "", entity_id, statistics_unit, old_metadata[1]["unit_of_measurement"], @@ -603,7 +577,6 @@ def list_statistic_ids( for state in entities: state_class = state.attributes[ATTR_STATE_CLASS] - device_class = state.attributes.get(ATTR_DEVICE_CLASS) state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) provided_statistics = DEFAULT_STATISTICS[state_class] @@ -620,21 +593,6 @@ def list_statistic_ids( ): continue - if device_class not in UNIT_CONVERTERS: - result[state.entity_id] = { - "has_mean": "mean" in provided_statistics, - "has_sum": "sum" in provided_statistics, - "name": None, - "source": RECORDER_DOMAIN, - "statistic_id": state.entity_id, - "unit_of_measurement": state_unit, - } - continue - - converter = UNIT_CONVERTERS[device_class] - if state_unit not in converter.VALID_UNITS: - continue - result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, @@ -643,6 +601,7 @@ def list_statistic_ids( "statistic_id": state.entity_id, "unit_of_measurement": state_unit, } + continue return result @@ -660,7 +619,6 @@ def validate_statistics( for state in sensor_states: entity_id = state.entity_id - device_class = state.attributes.get(ATTR_DEVICE_CLASS) state_class = state.attributes.get(ATTR_STATE_CLASS) state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -684,35 +642,30 @@ def validate_statistics( ) metadata_unit = metadata[1]["unit_of_measurement"] - if device_class not in UNIT_CONVERTERS: + converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit) + if not converter: if state_unit != metadata_unit: - # The unit has changed - issue_type = ( - "units_changed_can_convert" - if statistics.can_convert_units(metadata_unit, state_unit) - else "units_changed" - ) + # The unit has changed, and it's not possible to convert validation_result[entity_id].append( statistics.ValidationIssue( - issue_type, + "units_changed", { "statistic_id": entity_id, "state_unit": state_unit, "metadata_unit": metadata_unit, + "supported_unit": metadata_unit, }, ) ) - elif metadata_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS: - # The unit in metadata is not supported for this device class - valid_units = ", ".join( - sorted(UNIT_CONVERTERS[device_class].VALID_UNITS) - ) + elif state_unit not in converter.VALID_UNITS: + # The state unit can't be converted to the unit in metadata + valid_units = ", ".join(sorted(converter.VALID_UNITS)) validation_result[entity_id].append( statistics.ValidationIssue( - "unsupported_unit_metadata", + "units_changed", { "statistic_id": entity_id, - "device_class": device_class, + "state_unit": state_unit, "metadata_unit": metadata_unit, "supported_unit": valid_units, }, @@ -728,23 +681,6 @@ def validate_statistics( ) ) - if ( - state_class in STATE_CLASSES - and device_class in UNIT_CONVERTERS - and state_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS - ): - # The unit in the state is not supported for this device class - validation_result[entity_id].append( - statistics.ValidationIssue( - "unsupported_unit_state", - { - "statistic_id": entity_id, - "device_class": device_class, - "state_unit": state_unit, - }, - ) - ) - for statistic_id in sensor_statistic_ids - sensor_entity_ids: # There is no sensor matching the statistics_id validation_result[statistic_id].append( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 99aa3a3bf8e..8d9e34d005f 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -238,8 +238,8 @@ def test_compile_hourly_statistics_purged_state_changes( @pytest.mark.parametrize("attributes", [TEMPERATURE_SENSOR_ATTRIBUTES]) -def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes): - """Test compiling hourly statistics for unsupported sensor.""" +def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes): + """Test compiling hourly statistics for sensor with unit not matching device class.""" zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -286,6 +286,24 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, + { + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistic_id": "sensor.test2", + "statistics_unit_of_measurement": "invalid", + "unit_class": None, + }, + { + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistic_id": "sensor.test3", + "statistics_unit_of_measurement": None, + "unit_class": None, + }, { "statistic_id": "sensor.test6", "has_mean": True, @@ -320,6 +338,32 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "sum": None, } ], + "sensor.test2": [ + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": 13.05084745762712, + "min": -10.0, + "max": 30.0, + "last_reset": None, + "state": None, + "sum": None, + } + ], + "sensor.test3": [ + { + "statistic_id": "sensor.test3", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": 13.05084745762712, + "min": -10.0, + "max": 30.0, + "last_reset": None, + "state": None, + "sum": None, + } + ], "sensor.test6": [ { "statistic_id": "sensor.test6", @@ -835,32 +879,44 @@ def test_compile_hourly_sum_statistics_nan_inf_state( @pytest.mark.parametrize( - "entity_id,warning_1,warning_2", + "entity_id, device_class, state_unit, display_unit, statistics_unit, unit_class, offset, warning_1, warning_2", [ ( "sensor.test1", + "energy", + "kWh", + "kWh", + "kWh", + "energy", + 0, "", "bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue", ), ( "sensor.power_consumption", + "power", + "W", + "W", + "W", + "power", + 15, "from integration demo ", "bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+demo%22", ), ( "sensor.custom_sensor", + "energy", + "kWh", + "kWh", + "kWh", + "energy", + 0, "from integration test ", "report it to the custom integration author", ), ], ) @pytest.mark.parametrize("state_class", ["total_increasing"]) -@pytest.mark.parametrize( - "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", - [ - ("energy", "kWh", "kWh", "kWh", "energy", 1), - ], -) def test_compile_hourly_sum_statistics_negative_state( hass_recorder, caplog, @@ -873,7 +929,7 @@ def test_compile_hourly_sum_statistics_negative_state( display_unit, statistics_unit, unit_class, - factor, + offset, ): """Test compiling hourly statistics with negative states.""" zero = dt_util.utcnow() @@ -938,8 +994,8 @@ def test_compile_hourly_sum_statistics_negative_state( "mean": None, "min": None, "last_reset": None, - "state": approx(factor * seq[7]), - "sum": approx(factor * 15), # (15 - 10) + (10 - 0) + "state": approx(seq[7]), + "sum": approx(offset + 15), # (20 - 15) + (10 - 0) }, ] assert "Error while processing event StatisticsTask" not in caplog.text @@ -1889,7 +1945,7 @@ def test_compile_hourly_statistics_changing_units_1( do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) - assert "does not match the unit of already compiled" not in caplog.text + assert "can not be converted to the unit of previously" not in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { @@ -1922,8 +1978,8 @@ def test_compile_hourly_statistics_changing_units_1( do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) assert ( - "The unit of sensor.test1 (cats) does not match the unit of already compiled " - f"statistics ({display_unit})" in caplog.text + "The unit of sensor.test1 (cats) can not be converted to the unit of " + f"previously compiled statistics ({display_unit})" in caplog.text ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -3039,18 +3095,30 @@ def record_states(hass, zero, entity_id, attributes, seq=None): @pytest.mark.parametrize( - "units, attributes, unit", + "units, attributes, unit, unit2, supported_unit", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi"), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa"), + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "K", "K, °C, °F"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "K", "K, °C, °F"), + ( + IMPERIAL_SYSTEM, + PRESSURE_SENSOR_ATTRIBUTES, + "psi", + "bar", + "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", + ), + ( + METRIC_SYSTEM, + PRESSURE_SENSOR_ATTRIBUTES, + "Pa", + "bar", + "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", + ), ], ) -async def test_validate_statistics_supported_device_class( - hass, hass_ws_client, recorder_mock, units, attributes, unit +async def test_validate_statistics_unit_change_device_class( + hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit ): """Test validate_statistics.""" id = 1 @@ -3078,39 +3146,40 @@ async def test_validate_statistics_supported_device_class( # No statistics, no state - empty response await assert_validation_result(client, {}) - # No statistics, valid state - empty response + # No statistics, unit in state matching device class - empty response hass.states.async_set( "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) - # No statistics, invalid state - expect error + # No statistics, unit in state not matching device class - empty response hass.states.async_set( "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} ) await async_recorder_block_till_done(hass) - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "state_unit": "dogs", - "statistic_id": "sensor.test", - }, - "type": "unsupported_unit_state", - } - ], - } - await assert_validation_result(client, expected) + await assert_validation_result(client, {}) - # Statistics has run, invalid state - expect error + # Statistics has run, incompatible unit - expect error await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now) hass.states.async_set( "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} ) await async_recorder_block_till_done(hass) + expected = { + "sensor.test": [ + { + "data": { + "metadata_unit": unit, + "state_unit": "dogs", + "statistic_id": "sensor.test", + "supported_unit": supported_unit, + }, + "type": "units_changed", + } + ], + } await assert_validation_result(client, expected) # Valid state - empty response @@ -3125,6 +3194,18 @@ async def test_validate_statistics_supported_device_class( await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) + # Valid state in compatible unit - empty response + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + # Remove the state - empty response hass.states.async_remove("sensor.test") expected = { @@ -3144,7 +3225,7 @@ async def test_validate_statistics_supported_device_class( (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W, kW"), ], ) -async def test_validate_statistics_supported_device_class_2( +async def test_validate_statistics_unit_change_device_class_2( hass, hass_ws_client, recorder_mock, units, attributes, valid_units ): """Test validate_statistics.""" @@ -3173,56 +3254,144 @@ async def test_validate_statistics_supported_device_class_2( # No statistics, no state - empty response await assert_validation_result(client, {}) - # No statistics, valid state - empty response - initial_attributes = {"state_class": "measurement"} + # No statistics, no device class - empty response + initial_attributes = {"state_class": "measurement", "unit_of_measurement": "dogs"} hass.states.async_set("sensor.test", 10, attributes=initial_attributes) await hass.async_block_till_done() await assert_validation_result(client, {}) - # Statistics has run, device class set - expect error + # Statistics has run, device class set not matching unit - empty response do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", 12, attributes=attributes) - await hass.async_block_till_done() - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "metadata_unit": None, - "statistic_id": "sensor.test", - "supported_unit": valid_units, - }, - "type": "unsupported_unit_metadata", - } - ], - } - await assert_validation_result(client, expected) - - # Invalid state too, expect double errors hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + +@pytest.mark.parametrize( + "units, attributes, unit, unit2, supported_unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "K", "K, °C, °F"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "K", "K, °C, °F"), + ( + IMPERIAL_SYSTEM, + PRESSURE_SENSOR_ATTRIBUTES, + "psi", + "bar", + "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", + ), + ( + METRIC_SYSTEM, + PRESSURE_SENSOR_ATTRIBUTES, + "Pa", + "bar", + "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", + ), + ], +) +async def test_validate_statistics_unit_change_no_device_class( + hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit +): + """Test validate_statistics.""" + id = 1 + attributes = dict(attributes) + attributes.pop("device_class") + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, unit in state matching device class - empty response + hass.states.async_set( + "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # No statistics, unit in state not matching device class - empty response + hass.states.async_set( + "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Statistics has run, incompatible unit - expect error + await async_recorder_block_till_done(hass) + do_adhoc_statistics(hass, start=now) + hass.states.async_set( + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} ) await async_recorder_block_till_done(hass) expected = { "sensor.test": [ { "data": { - "device_class": attributes["device_class"], - "metadata_unit": None, - "statistic_id": "sensor.test", - "supported_unit": valid_units, - }, - "type": "unsupported_unit_metadata", - }, - { - "data": { - "device_class": attributes["device_class"], + "metadata_unit": unit, "state_unit": "dogs", "statistic_id": "sensor.test", + "supported_unit": supported_unit, }, - "type": "unsupported_unit_state", - }, + "type": "units_changed", + } + ], + } + await assert_validation_result(client, expected) + + # Valid state - empty response + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Valid state in compatible unit - empty response + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Remove the state - empty response + hass.states.async_remove("sensor.test") + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "no_state", + } ], } await assert_validation_result(client, expected) @@ -3473,7 +3642,7 @@ async def test_validate_statistics_sensor_removed( "attributes", [BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES], ) -async def test_validate_statistics_unsupported_device_class( +async def test_validate_statistics_unit_change_no_conversion( hass, recorder_mock, hass_ws_client, attributes ): """Test validate_statistics.""" @@ -3553,6 +3722,7 @@ async def test_validate_statistics_unsupported_device_class( "metadata_unit": "dogs", "state_unit": attributes.get("unit_of_measurement"), "statistic_id": "sensor.test", + "supported_unit": "dogs", }, "type": "units_changed", } @@ -3573,124 +3743,7 @@ async def test_validate_statistics_unsupported_device_class( await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) - # Remove the state - empty response - hass.states.async_remove("sensor.test") - expected = { - "sensor.test": [ - { - "data": {"statistic_id": "sensor.test"}, - "type": "no_state", - } - ], - } - await assert_validation_result(client, expected) - - -@pytest.mark.parametrize( - "attributes", - [KW_SENSOR_ATTRIBUTES], -) -async def test_validate_statistics_unsupported_device_class_2( - hass, recorder_mock, hass_ws_client, attributes -): - """Test validate_statistics.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - async def assert_statistic_ids(expected_result): - with session_scope(hass=hass) as session: - db_states = list(session.query(StatisticsMeta)) - assert len(db_states) == len(expected_result) - for i in range(len(db_states)): - assert db_states[i].statistic_id == expected_result[i]["statistic_id"] - assert ( - db_states[i].unit_of_measurement - == expected_result[i]["unit_of_measurement"] - ) - - now = dt_util.utcnow() - - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - client = await hass_ws_client() - - # No statistics, no state - empty response - await assert_validation_result(client, {}) - - # No statistics, original unit - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) - await assert_validation_result(client, {}) - - # No statistics, changed unit - empty response - hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "W"}} - ) - await assert_validation_result(client, {}) - - # Run statistics, no statistics will be generated because of conflicting units - await async_recorder_block_till_done(hass) - do_adhoc_statistics(hass, start=now) - await async_recorder_block_till_done(hass) - await assert_statistic_ids([]) - - # No statistics, changed unit - empty response - hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "W"}} - ) - await assert_validation_result(client, {}) - - # Run statistics one hour later, only the "W" state will be considered - await async_recorder_block_till_done(hass) - do_adhoc_statistics(hass, start=now + timedelta(hours=1)) - await async_recorder_block_till_done(hass) - await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": "W"}] - ) - await assert_validation_result(client, {}) - - # Change back to original unit - expect error - hass.states.async_set("sensor.test", 13, attributes=attributes) - await async_recorder_block_till_done(hass) - expected = { - "sensor.test": [ - { - "data": { - "metadata_unit": "W", - "state_unit": "kW", - "statistic_id": "sensor.test", - }, - "type": "units_changed_can_convert", - } - ], - } - await assert_validation_result(client, expected) - - # Changed unit - empty response - hass.states.async_set( - "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": "W"}} - ) - await async_recorder_block_till_done(hass) - await assert_validation_result(client, {}) - - # Valid state, statistic runs again - empty response - await async_recorder_block_till_done(hass) - do_adhoc_statistics(hass, start=now) - await async_recorder_block_till_done(hass) - await assert_validation_result(client, {}) - - # Remove the state - empty response + # Remove the state - expect error hass.states.async_remove("sensor.test") expected = { "sensor.test": [ From 5f3774219704c10523bbe6817ec88745a2cc30c8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:02:24 -0400 Subject: [PATCH 119/183] ZHA radio migration: reset the old adapter (#79663) --- homeassistant/components/zha/config_flow.py | 84 ++++++++-- homeassistant/components/zha/strings.json | 16 ++ .../components/zha/translations/en.json | 39 ++--- tests/components/zha/test_config_flow.py | 148 ++++++++++++++---- 4 files changed, 219 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index ce2080e4a13..85f03b9f1f5 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,6 +1,7 @@ """Config flow for ZHA.""" from __future__ import annotations +import asyncio import collections import contextlib import copy @@ -65,8 +66,16 @@ FORMATION_UPLOAD_MANUAL_BACKUP = "upload_manual_backup" CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup" OVERWRITE_COORDINATOR_IEEE = "overwrite_coordinator_ieee" +OPTIONS_INTENT_MIGRATE = "intent_migrate" +OPTIONS_INTENT_RECONFIGURE = "intent_reconfigure" + UPLOADED_BACKUP_FILE = "uploaded_backup_file" +DEFAULT_ZHA_ZEROCONF_PORT = 6638 +ESPHOME_API_PORT = 6053 + +CONNECT_DELAY_S = 1.0 + _LOGGER = logging.getLogger(__name__) @@ -159,6 +168,7 @@ class BaseZhaFlow(FlowHandler): yield app finally: await app.disconnect() + await asyncio.sleep(CONNECT_DELAY_S) async def _restore_backup( self, backup: zigpy.backups.NetworkBackup, **kwargs: Any @@ -628,14 +638,21 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN # Hostname is format: livingroom.local. local_name = discovery_info.hostname[:-1] - radio_type = discovery_info.properties.get("radio_type") or local_name + port = discovery_info.port or DEFAULT_ZHA_ZEROCONF_PORT + + # Fix incorrect port for older TubesZB devices + if "tube" in local_name and port == ESPHOME_API_PORT: + port = DEFAULT_ZHA_ZEROCONF_PORT + + if "radio_type" in discovery_info.properties: + self._radio_type = RadioType[discovery_info.properties["radio_type"]] + elif "efr32" in local_name: + self._radio_type = RadioType.ezsp + else: + self._radio_type = RadioType.znp + node_name = local_name[: -len(".local")] - host = discovery_info.host - port = discovery_info.port - if local_name.startswith("tube") or "efr32" in local_name: - # This is hard coded to work with legacy devices - port = 6638 - device_path = f"socket://{host}:{port}" + device_path = f"socket://{discovery_info.host}:{port}" if current_entry := await self.async_set_unique_id(node_name): self._abort_if_unique_id_configured( @@ -651,13 +668,6 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN self._title = device_path self._device_path = device_path - if "efr32" in radio_type: - self._radio_type = RadioType.ezsp - elif "zigate" in radio_type: - self._radio_type = RadioType.zigate - else: - self._radio_type = RadioType.znp - return await self.async_step_confirm() async def async_step_hardware( @@ -720,10 +730,54 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): # ZHA is not running pass - return await self.async_step_choose_serial_port() + return await self.async_step_prompt_migrate_or_reconfigure() return self.async_show_form(step_id="init") + async def async_step_prompt_migrate_or_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm if we are migrating adapters or just re-configuring.""" + + return self.async_show_menu( + step_id="prompt_migrate_or_reconfigure", + menu_options=[ + OPTIONS_INTENT_RECONFIGURE, + OPTIONS_INTENT_MIGRATE, + ], + ) + + async def async_step_intent_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Virtual step for when the user is reconfiguring the integration.""" + return await self.async_step_choose_serial_port() + + async def async_step_intent_migrate( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm the user wants to reset their current radio.""" + + if user_input is not None: + # Reset the current adapter + async with self._connect_zigpy_app() as app: + await app.reset_network_info() + + return await self.async_step_instruct_unplug() + + return self.async_show_form(step_id="intent_migrate") + + async def async_step_instruct_unplug( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Instruct the user to unplug the current radio, if possible.""" + + if user_input is not None: + # Now that the old radio is gone, we can scan for serial ports again + return await self.async_step_choose_serial_port() + + return self.async_show_form(step_id="instruct_unplug") + async def _async_create_radio_entity(self): """Re-implementation of the base flow's final step to update the config.""" device_settings = self._device_settings.copy() diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 3901f9f9439..240f3c4ee83 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -76,6 +76,22 @@ "title": "Reconfigure ZHA", "description": "ZHA will be stopped. Do you wish to continue?" }, + "prompt_migrate_or_reconfigure": { + "title": "Migrate or re-configure", + "description": "Are you migrating to a new radio or re-configuring the current radio?", + "menu_options": { + "intent_migrate": "Migrate to a new radio", + "intent_reconfigure": "Re-configure the current radio" + } + }, + "intent_migrate": { + "title": "Migrate to a new radio", + "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?" + }, + "instruct_unplug": { + "title": "Unplug your old radio", + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it." + }, "choose_serial_port": { "title": "[%key:component::zha::config::step::choose_serial_port::title%]", "data": { diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index adf89983256..68d36b7fac7 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -64,35 +64,12 @@ "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", "title": "Overwrite Radio IEEE Address" }, - "pick_radio": { - "data": { - "radio_type": "Radio Type" - }, - "description": "Pick a type of your Zigbee radio", - "title": "Radio Type" - }, - "port_config": { - "data": { - "baudrate": "port speed", - "flow_control": "data flow control", - "path": "Serial device path" - }, - "description": "Enter port specific settings", - "title": "Settings" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Upload a file" }, "description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.", "title": "Upload a Manual Backup" - }, - "user": { - "data": { - "path": "Serial Device Path" - }, - "description": "Select serial port for Zigbee radio", - "title": "ZHA" } } }, @@ -212,6 +189,14 @@ "description": "ZHA will be stopped. Do you wish to continue?", "title": "Reconfigure ZHA" }, + "instruct_unplug": { + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.", + "title": "Unplug your old radio" + }, + "intent_migrate": { + "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?", + "title": "Migrate to a new radio" + }, "manual_pick_radio_type": { "data": { "radio_type": "Radio Type" @@ -235,6 +220,14 @@ "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", "title": "Overwrite Radio IEEE Address" }, + "prompt_migrate_or_reconfigure": { + "description": "Are you migrating to a new radio or re-configuring the current radio?", + "menu_options": { + "intent_migrate": "Migrate to a new radio", + "intent_reconfigure": "Re-configure the current radio" + }, + "title": "Migrate or re-configure" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Upload a file" diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 5fc4b232634..725f9cc0917 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -46,6 +46,13 @@ def disable_platform_only(): yield +@pytest.fixture(autouse=True) +def reduce_reconnect_timeout(): + """Reduces reconnect timeout to speed up tests.""" + with patch("homeassistant.components.zha.config_flow.CONNECT_DELAY_S", 0.01): + yield + + @pytest.fixture(autouse=True) def mock_app(): """Mock zigpy app interface.""" @@ -230,10 +237,10 @@ async def test_efr32_via_zeroconf(hass): await hass.async_block_till_done() assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "socket://192.168.1.200:6638" + assert result3["title"] == "socket://192.168.1.200:1234" assert result3["data"] == { CONF_DEVICE: { - CONF_DEVICE_PATH: "socket://192.168.1.200:6638", + CONF_DEVICE_PATH: "socket://192.168.1.200:1234", CONF_BAUDRATE: 115200, CONF_FLOWCONTROL: "software", }, @@ -1476,21 +1483,28 @@ async def test_options_flow_defaults(async_setup_entry, async_unload_effect, has # Unload it ourselves entry.state = config_entries.ConfigEntryState.NOT_LOADED + # Reconfigure ZHA + assert result1["step_id"] == "prompt_migrate_or_reconfigure" + result2 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE}, + ) + # Current path is the default - assert result1["step_id"] == "choose_serial_port" - assert "/dev/ttyUSB0" in result1["data_schema"]({})[CONF_DEVICE_PATH] + assert result2["step_id"] == "choose_serial_port" + assert "/dev/ttyUSB0" in result2["data_schema"]({})[CONF_DEVICE_PATH] # Autoprobing fails, we have to manually choose the radio type - result2 = await hass.config_entries.options.async_configure( + result3 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={} ) # Current radio type is the default - assert result2["step_id"] == "manual_pick_radio_type" - assert result2["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description + assert result3["step_id"] == "manual_pick_radio_type" + assert result3["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description # Continue on to port settings - result3 = await hass.config_entries.options.async_configure( + result4 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={ CONF_RADIO_TYPE: RadioType.znp.description, @@ -1498,12 +1512,12 @@ async def test_options_flow_defaults(async_setup_entry, async_unload_effect, has ) # The defaults match our current settings - assert result3["step_id"] == "manual_port_config" - assert result3["data_schema"]({}) == entry.data[CONF_DEVICE] + assert result4["step_id"] == "manual_port_config" + assert result4["data_schema"]({}) == entry.data[CONF_DEVICE] with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)): # Change the serial port path - result4 = await hass.config_entries.options.async_configure( + result5 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={ # Change everything @@ -1514,18 +1528,18 @@ async def test_options_flow_defaults(async_setup_entry, async_unload_effect, has ) # The radio has been detected, we can move on to creating the config entry - assert result4["step_id"] == "choose_formation_strategy" + assert result5["step_id"] == "choose_formation_strategy" async_setup_entry.assert_not_called() - result5 = await hass.config_entries.options.async_configure( + result6 = await hass.config_entries.options.async_configure( result1["flow_id"], user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, ) await hass.async_block_till_done() - assert result5["type"] == FlowResultType.CREATE_ENTRY - assert result5["data"] == {} + assert result6["type"] == FlowResultType.CREATE_ENTRY + assert result6["data"] == {} # The updated entry contains correct settings assert entry.data == { @@ -1581,33 +1595,39 @@ async def test_options_flow_defaults_socket(hass): flow["flow_id"], user_input={} ) - # Radio path must be manually entered - assert result1["step_id"] == "choose_serial_port" - assert result1["data_schema"]({})[CONF_DEVICE_PATH] == config_flow.CONF_MANUAL_PATH - + assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( - flow["flow_id"], user_input={} + flow["flow_id"], + user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE}, ) - # Current radio type is the default - assert result2["step_id"] == "manual_pick_radio_type" - assert result2["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description + # Radio path must be manually entered + assert result2["step_id"] == "choose_serial_port" + assert result2["data_schema"]({})[CONF_DEVICE_PATH] == config_flow.CONF_MANUAL_PATH - # Continue on to port settings result3 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={} ) + # Current radio type is the default + assert result3["step_id"] == "manual_pick_radio_type" + assert result3["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description + + # Continue on to port settings + result4 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + # The defaults match our current settings - assert result3["step_id"] == "manual_port_config" - assert result3["data_schema"]({}) == entry.data[CONF_DEVICE] + assert result4["step_id"] == "manual_port_config" + assert result4["data_schema"]({}) == entry.data[CONF_DEVICE] with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)): - result4 = await hass.config_entries.options.async_configure( + result5 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={} ) - assert result4["step_id"] == "choose_formation_strategy" + assert result5["step_id"] == "choose_formation_strategy" @patch("homeassistant.components.zha.async_setup_entry", return_value=True) @@ -1643,14 +1663,82 @@ async def test_options_flow_restarts_running_zha_if_cancelled(async_setup_entry, entry.state = config_entries.ConfigEntryState.NOT_LOADED + assert result1["step_id"] == "prompt_migrate_or_reconfigure" + result2 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE}, + ) + # Radio path must be manually entered - assert result1["step_id"] == "choose_serial_port" + assert result2["step_id"] == "choose_serial_port" async_setup_entry.reset_mock() # Abort the flow - hass.config_entries.options.async_abort(result1["flow_id"]) + hass.config_entries.options.async_abort(result2["flow_id"]) await hass.async_block_till_done() # ZHA was set up once more async_setup_entry.assert_called_once_with(hass, entry) + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_options_flow_migration_reset_old_adapter(hass, mock_app): + """Test options flow for migrating from an old radio.""" + + entry = MockConfigEntry( + version=config_flow.ZhaConfigFlowHandler.VERSION, + domain=DOMAIN, + data={ + CONF_DEVICE: { + CONF_DEVICE_PATH: "/dev/serial/by-id/old_radio", + CONF_BAUDRATE: 12345, + CONF_FLOWCONTROL: None, + }, + CONF_RADIO_TYPE: "znp", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + flow = await hass.config_entries.options.async_init(entry.entry_id) + + # ZHA gets unloaded + with patch( + "homeassistant.config_entries.ConfigEntries.async_unload", return_value=True + ): + result1 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + entry.state = config_entries.ConfigEntryState.NOT_LOADED + + assert result1["step_id"] == "prompt_migrate_or_reconfigure" + result2 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={"next_step_id": config_flow.OPTIONS_INTENT_MIGRATE}, + ) + + # User must explicitly approve radio reset + assert result2["step_id"] == "intent_migrate" + + mock_app.reset_network_info = AsyncMock() + + result3 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={}, + ) + + mock_app.reset_network_info.assert_awaited_once() + + # Now we can unplug the old radio + assert result3["step_id"] == "instruct_unplug" + + # And move on to choosing the new radio + result4 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={}, + ) + assert result4["step_id"] == "choose_serial_port" From 3bd4d66b2ebce1a70a613728de0c46b18f850102 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Oct 2022 16:32:29 -1000 Subject: [PATCH 120/183] Fix bluetooth diagnostics on macos (#79680) * Fix bluetooth diagnostics on macos The pyobjc objects cannot be pickled which cases dataclasses asdict to raise an exception when trying to do the deepcopy We now implement our own as_dict to avoid this problem * add cover --- homeassistant/components/bluetooth/manager.py | 5 +-- homeassistant/components/bluetooth/models.py | 19 +++++++++ .../components/bluetooth/test_diagnostics.py | 40 +++++++++++++++++-- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 37c24423231..f0152f5ae5e 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable -from dataclasses import asdict from datetime import datetime, timedelta import itertools import logging @@ -185,11 +184,11 @@ class BluetoothManager: "adapters": self._adapters, "scanners": scanner_diagnostics, "connectable_history": [ - asdict(service_info) + service_info.as_dict() for service_info in self._connectable_history.values() ], "history": [ - asdict(service_info) for service_info in self._history.values() + service_info.as_dict() for service_info in self._history.values() ], } diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index d93f8efc1e2..9e93ea4d142 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -53,6 +53,25 @@ class BluetoothServiceInfoBleak(BluetoothServiceInfo): connectable: bool time: float + def as_dict(self) -> dict[str, Any]: + """Return as dict. + + The dataclass asdict method is not used because + it will try to deepcopy pyobjc data which will fail. + """ + return { + "name": self.name, + "address": self.address, + "rssi": self.rssi, + "manufacturer_data": self.manufacturer_data, + "service_data": self.service_data, + "service_uuids": self.service_uuids, + "source": self.source, + "advertisement": self.advertisement, + "connectable": self.connectable, + "time": self.time, + } + class BluetoothScanningMode(Enum): """The mode of scanning for bluetooth devices.""" diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index d641cae9c7c..1da071a76ab 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -3,11 +3,13 @@ from unittest.mock import ANY, patch -from bleak.backends.scanner import BLEDevice +from bleak.backends.scanner import AdvertisementData, BLEDevice from homeassistant.components import bluetooth from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS +from . import inject_advertisement + from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -158,6 +160,10 @@ async def test_diagnostics_macos( # because we cannot import the scanner class directly without it throwing an # error if the test is not running on linux since we won't have the correct # deps installed when testing on MacOS. + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) with patch( "homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices", @@ -180,6 +186,8 @@ async def test_diagnostics_macos( assert await hass.config_entries.async_setup(entry1.entry_id) await hass.async_block_till_done() + inject_advertisement(hass, switchbot_device, switchbot_adv) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1) assert diag == { "adapters": { @@ -197,8 +205,34 @@ async def test_diagnostics_macos( "sw_version": ANY, } }, - "connectable_history": [], - "history": [], + "connectable_history": [ + { + "address": "44:44:33:11:23:45", + "advertisement": ANY, + "connectable": True, + "manufacturer_data": ANY, + "name": "wohand", + "rssi": 0, + "service_data": {}, + "service_uuids": [], + "source": "local", + "time": ANY, + } + ], + "history": [ + { + "address": "44:44:33:11:23:45", + "advertisement": ANY, + "connectable": True, + "manufacturer_data": ANY, + "name": "wohand", + "rssi": 0, + "service_data": {}, + "service_uuids": [], + "source": "local", + "time": ANY, + } + ], "scanners": [ { "adapter": "Core Bluetooth", From 998d13499ce72b6a91966260042d5aa430199217 Mon Sep 17 00:00:00 2001 From: Matthew Simpson Date: Thu, 6 Oct 2022 16:01:27 +0100 Subject: [PATCH 121/183] Bump btsmarthub_devicelist to 0.2.3 (#79705) * Bump btsmarthub_devicelist This PR bumps the btsmarthub_devicelist version to correct an issue experienced by a recent firmware upgrade to the SmartHub2. * Bump btsmarthub_devicelist to 0.2.3 This version bump fixes an issue where BT SmartHub2 devices cannot be correctly autodetected. The current workaround is to specifiy it manually, which isn't great UX (and did previously work until a recent firmware upgrade). I've also taken the opportunity to reassign ownership of the component to myself as @jxwolstenholme no longer has a SmartHub so cannot do manual testing and also has no need to use the component anymore. --- CODEOWNERS | 2 +- homeassistant/components/bt_smarthub/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5c39337af74..a01d358208b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -162,7 +162,7 @@ build.json @home-assistant/supervisor /tests/components/brunt/ @eavanvalkenburg /homeassistant/components/bsblan/ @liudger /tests/components/bsblan/ @liudger -/homeassistant/components/bt_smarthub/ @jxwolstenholme +/homeassistant/components/bt_smarthub/ @typhoon2099 /homeassistant/components/bthome/ @Ernst79 /tests/components/bthome/ @Ernst79 /homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221 diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index fb34117eb6b..4519ee517c3 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -2,8 +2,8 @@ "domain": "bt_smarthub", "name": "BT Smart Hub", "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", - "requirements": ["btsmarthub_devicelist==0.2.2"], - "codeowners": ["@jxwolstenholme"], + "requirements": ["btsmarthub_devicelist==0.2.3"], + "codeowners": ["@typhoon2099"], "iot_class": "local_polling", "loggers": ["btsmarthub_devicelist"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1359a9c1b45..f2060f0c908 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -475,7 +475,7 @@ bthome-ble==1.2.2 bthomehub5-devicelist==0.1.1 # homeassistant.components.bt_smarthub -btsmarthub_devicelist==0.2.2 +btsmarthub_devicelist==0.2.3 # homeassistant.components.buienradar buienradar==1.0.5 From 35a69cb2532d4494f78f17024168d8afaf69a0a1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 Oct 2022 20:01:54 +0200 Subject: [PATCH 122/183] Correct how unit used for statistics is determined (#79725) --- homeassistant/components/sensor/recorder.py | 35 ++++++++++++--------- tests/components/sensor/test_recorder.py | 24 +++++++------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 1a72444c758..beae06f78ff 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -149,13 +149,20 @@ def _normalize_states( state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if state_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER or ( - old_metadata - and old_metadata["unit_of_measurement"] - not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER + statistics_unit: str | None + if not old_metadata: + # We've not seen this sensor before, the first valid state determines the unit + # used for statistics + statistics_unit = state_unit + else: + # We have seen this sensor before, use the unit from metadata + statistics_unit = old_metadata["unit_of_measurement"] + + if ( + not statistics_unit + or statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER ): - # We're either not normalizing this device class or this entity is not stored - # in a unit which can be converted, return the states as they are + # The unit used by this sensor doesn't support unit conversion all_units = _get_units(fstates) if len(all_units) > 1: @@ -182,13 +189,9 @@ def _normalize_states( state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) return state_unit, state_unit, fstates - converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[state_unit] + converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[statistics_unit] valid_fstates: list[tuple[float, State]] = [] - statistics_unit: str | None = None - if old_metadata: - statistics_unit = old_metadata["unit_of_measurement"] - for fstate, state in fstates: state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # Exclude states with unsupported unit from statistics @@ -198,14 +201,18 @@ def _normalize_states( if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]: hass.data[WARN_UNSUPPORTED_UNIT].add(entity_id) _LOGGER.warning( - "%s has unit %s which can't be converted to %s", + "The unit of %s (%s) can not be converted to the unit of previously " + "compiled statistics (%s). Generation of long term statistics " + "will be suppressed unless the unit changes back to %s or a " + "compatible unit. " + "Go to %s to fix this", entity_id, state_unit, statistics_unit, + statistics_unit, + LINK_DEV_STATISTICS, ) continue - if statistics_unit is None: - statistics_unit = state_unit valid_fstates.append( ( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 8d9e34d005f..0a72dcf6fcd 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1900,12 +1900,13 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): @pytest.mark.parametrize( - "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", + "device_class, state_unit, state_unit2, unit_class, mean, min, max", [ - (None, None, None, None, None, 13.050847, -10, 30), - (None, "%", "%", "%", None, 13.050847, -10, 30), - ("battery", "%", "%", "%", None, 13.050847, -10, 30), - ("battery", None, None, None, None, 13.050847, -10, 30), + (None, None, "cats", None, 13.050847, -10, 30), + (None, "%", "cats", None, 13.050847, -10, 30), + ("battery", "%", "cats", None, 13.050847, -10, 30), + ("battery", None, "cats", None, 13.050847, -10, 30), + (None, "kW", "Wh", "power", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_1( @@ -1913,8 +1914,7 @@ def test_compile_hourly_statistics_changing_units_1( caplog, device_class, state_unit, - display_unit, - statistics_unit, + state_unit2, unit_class, mean, min, @@ -1931,7 +1931,7 @@ def test_compile_hourly_statistics_changing_units_1( "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) - attributes["unit_of_measurement"] = "cats" + attributes["unit_of_measurement"] = state_unit2 four, _states = record_states( hass, zero + timedelta(minutes=5), "sensor.test1", attributes ) @@ -1954,7 +1954,7 @@ def test_compile_hourly_statistics_changing_units_1( "has_sum": False, "name": None, "source": "recorder", - "statistics_unit_of_measurement": statistics_unit, + "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, ] @@ -1978,8 +1978,8 @@ def test_compile_hourly_statistics_changing_units_1( do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) assert ( - "The unit of sensor.test1 (cats) can not be converted to the unit of " - f"previously compiled statistics ({display_unit})" in caplog.text + f"The unit of sensor.test1 ({state_unit2}) can not be converted to the unit of " + f"previously compiled statistics ({state_unit})" in caplog.text ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -1989,7 +1989,7 @@ def test_compile_hourly_statistics_changing_units_1( "has_sum": False, "name": None, "source": "recorder", - "statistics_unit_of_measurement": statistics_unit, + "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, ] From aacae6b9bf759888c5bc1e7c06cdf569669a3c07 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 6 Oct 2022 20:01:18 +0200 Subject: [PATCH 123/183] Update frontend to 20221006.0 (#79745) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index e6d5f63272d..6f243da444a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221005.0"], + "requirements": ["home-assistant-frontend==20221006.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2f637ba61f1..f493034171f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.24.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221005.0 +home-assistant-frontend==20221006.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index f2060f0c908..caf9c2209bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,7 +865,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221005.0 +home-assistant-frontend==20221006.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba5ca682243..33927e272e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -645,7 +645,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221005.0 +home-assistant-frontend==20221006.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 570270b9ea71fcd576129be74add4e1d9212e73c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Oct 2022 14:13:51 -0400 Subject: [PATCH 124/183] Bumped version to 2022.10.1 --- 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 139b3a157b2..cb7eb689b2d 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 = 10 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __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 8152dd57cfc..287e1c6d627 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0" +version = "2022.10.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 779d805b5a45093bb6b18e8ee0b2437737dfd083 Mon Sep 17 00:00:00 2001 From: John Levermore Date: Sat, 8 Oct 2022 19:53:32 +0100 Subject: [PATCH 125/183] Fix london_underground TUBE_LINES to match current API output (#79410) Fix: Update london_underground component with updated TUBE_LINES list to match current API output --- homeassistant/components/london_underground/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index 96ff9bc5056..b111fb8be6c 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -39,13 +39,13 @@ TUBE_LINES = [ "Circle", "District", "DLR", + "Elizabeth line", "Hammersmith & City", "Jubilee", "London Overground", "Metropolitan", "Northern", "Piccadilly", - "TfL Rail", "Victoria", "Waterloo & City", ] From 11b351f0150a2782b8353f1902f0620b334a786a Mon Sep 17 00:00:00 2001 From: Bert Melis Date: Sat, 8 Oct 2022 15:36:49 +0200 Subject: [PATCH 126/183] Process abbreviated availability options in mqtt discovery payload (#79712) Expand availability in mqtt discovery payload --- homeassistant/components/mqtt/discovery.py | 8 ++++++++ tests/components/mqtt/test_discovery.py | 12 ++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 23453e146ed..92ad50b7b4a 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -139,6 +139,14 @@ async def async_start( # noqa: C901 key = DEVICE_ABBREVIATIONS.get(key, key) device[key] = device.pop(abbreviated_key) + if CONF_AVAILABILITY in payload: + for availability_conf in cv.ensure_list(payload[CONF_AVAILABILITY]): + if isinstance(availability_conf, dict): + for key in list(availability_conf): + abbreviated_key = key + key = ABBREVIATIONS.get(key, key) + availability_conf[key] = availability_conf.pop(abbreviated_key) + if TOPIC_BASE in payload: base = payload.pop(TOPIC_BASE) for key, value in payload.items(): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 50c0a50bd40..77d7093830e 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -945,9 +945,9 @@ async def test_discovery_expansion(hass, mqtt_mock_entry_no_yaml_config, caplog) ' "payload_not_available": "not_available"' " }," " {" - ' "topic":"avail_item2/~",' - ' "payload_available": "available",' - ' "payload_not_available": "not_available"' + ' "t":"avail_item2/~",' + ' "pl_avail": "available",' + ' "pl_not_avail": "not_available"' " }" " ]," ' "dev":{' @@ -999,9 +999,9 @@ async def test_discovery_expansion_2(hass, mqtt_mock_entry_no_yaml_config, caplo ' "stat_t": "test_topic/~",' ' "cmd_t": "~/test_topic",' ' "availability": {' - ' "topic":"~/avail_item1",' - ' "payload_available": "available",' - ' "payload_not_available": "not_available"' + ' "t":"~/avail_item1",' + ' "pl_avail": "available",' + ' "pl_not_avail": "not_available"' " }," ' "dev":{' ' "ids":["5706DF"],' From 66d14ec1ccaf902e9e4f7a25bcd8355fdac957df Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 7 Oct 2022 01:22:15 +0200 Subject: [PATCH 127/183] Show all valid heatpump selections (#79756) Iterate over the keys of the member dunder --- homeassistant/components/nibe_heatpump/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py index 28fafdb3a37..412b44d69a1 100644 --- a/homeassistant/components/nibe_heatpump/config_flow.py +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -29,7 +29,7 @@ from .const import ( STEP_USER_DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_MODEL): vol.In([e.name for e in Model]), + vol.Required(CONF_MODEL): vol.In(list(Model.__members__)), vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_LISTENING_PORT): cv.port, vol.Required(CONF_REMOTE_READ_PORT): cv.port, From 316c5f8cc34ca91091a907a65b7f22f171ed7b3a Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 7 Oct 2022 01:18:13 +0200 Subject: [PATCH 128/183] Bump pydaikin version (#79761) bump pydaikin version --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 28bfec14760..0657f597a5d 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.7.0"], + "requirements": ["pydaikin==2.7.2"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index caf9c2209bb..32ea83cc4d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1499,7 +1499,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.7.0 +pydaikin==2.7.2 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33927e272e1..3e514bec018 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1057,7 +1057,7 @@ pycomfoconnect==0.4 pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.7.0 +pydaikin==2.7.2 # homeassistant.components.deconz pydeconz==104 From 81783fd52b9762e77ea101a67abe6b283a480734 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Oct 2022 16:40:40 -1000 Subject: [PATCH 129/183] Fix Bluetooth failover when esphome device unexpectedly disconnects (#79769) --- .../components/esphome/bluetooth/__init__.py | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index 4f3235676a4..b4d5fdbd04d 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -1,6 +1,7 @@ """Bluetooth support for esphome.""" from __future__ import annotations +from collections.abc import Callable import logging from aioesphomeapi import APIClient @@ -11,14 +12,8 @@ from homeassistant.components.bluetooth import ( async_register_scanner, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import ( - CALLBACK_TYPE, - HomeAssistant, - async_get_hass, - callback as hass_callback, -) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback -from ..domain_data import DomainData from ..entry_data import RuntimeEntryData from .client import ESPHomeClient from .scanner import ESPHomeScanner @@ -27,18 +22,23 @@ _LOGGER = logging.getLogger(__name__) @hass_callback -def async_can_connect(source: str) -> bool: - """Check if a given source can make another connection.""" - domain_data = DomainData.get(async_get_hass()) - entry = domain_data.get_by_unique_id(source) - entry_data = domain_data.get_entry_data(entry) - _LOGGER.debug( - "Checking if %s can connect, available=%s, ble_connections_free=%s", - source, - entry_data.available, - entry_data.ble_connections_free, - ) - return bool(entry_data.available and entry_data.ble_connections_free) +def _async_can_connect_factory( + entry_data: RuntimeEntryData, source: str +) -> Callable[[], bool]: + """Create a can_connect function for a specific RuntimeEntryData instance.""" + + @hass_callback + def _async_can_connect() -> bool: + """Check if a given source can make another connection.""" + _LOGGER.debug( + "Checking if %s can connect, available=%s, ble_connections_free=%s", + source, + entry_data.available, + entry_data.ble_connections_free, + ) + return bool(entry_data.available and entry_data.ble_connections_free) + + return _async_can_connect async def async_connect_scanner( @@ -63,7 +63,7 @@ async def async_connect_scanner( connector = HaBluetoothConnector( client=ESPHomeClient, source=source, - can_connect=lambda: async_can_connect(source), + can_connect=_async_can_connect_factory(entry_data, source), ) scanner = ESPHomeScanner(hass, source, new_info_callback, connector, connectable) unload_callbacks = [ From 1143ede4db1d56f64a63641e0090936453132dce Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 6 Oct 2022 23:29:34 -0500 Subject: [PATCH 130/183] Fix state updating for crossfade switch on Sonos (#79776) --- homeassistant/components/sonos/speaker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 98984eedc03..38d37e7cfd4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -489,7 +489,10 @@ class SonosSpeaker: return if crossfade := event.variables.get("current_crossfade_mode"): - self.cross_fade = bool(int(crossfade)) + crossfade = bool(int(crossfade)) + if self.cross_fade != crossfade: + self.cross_fade = crossfade + self.async_write_entity_states() # Missing transport_state indicates a transient error if (new_status := event.variables.get("transport_state")) is None: From fc4ffc748cf74b14c29a8e54e836ac02c36ceb2c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 Oct 2022 14:23:53 +0200 Subject: [PATCH 131/183] Revert "Improve device_automation trigger validation" (#79778) Revert "Improve device_automation trigger validation (#75044)" This reverts commit 55b036ec5ef570b146a6126408da929dd66e431e. --- .../components/device_automation/action.py | 6 +-- .../components/device_automation/condition.py | 4 +- .../components/device_automation/trigger.py | 5 +- .../components/rfxtrx/device_action.py | 1 + .../components/device_automation/test_init.py | 51 ++----------------- .../components/webostv/test_device_trigger.py | 3 +- 6 files changed, 14 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index 432ff2fdb7d..081b6bb283a 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from . import DeviceAutomationType, async_get_device_automation_platform @@ -52,15 +51,14 @@ async def async_validate_action_config( ) -> ConfigType: """Validate config.""" try: - config = cv.DEVICE_ACTION_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.ACTION ) if hasattr(platform, "async_validate_action_config"): return await platform.async_validate_action_config(hass, config) return cast(ConfigType, platform.ACTION_SCHEMA(config)) - except (vol.Invalid, InvalidDeviceAutomationConfig) as err: - raise vol.Invalid("invalid action configuration: " + str(err)) from err + except InvalidDeviceAutomationConfig as err: + raise vol.Invalid(str(err) or "Invalid action configuration") from err async def async_call_action_from_config( diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index 3b0a5263f9e..d656908f4be 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -58,8 +58,8 @@ async def async_validate_condition_config( if hasattr(platform, "async_validate_condition_config"): return await platform.async_validate_condition_config(hass, config) return cast(ConfigType, platform.CONDITION_SCHEMA(config)) - except (vol.Invalid, InvalidDeviceAutomationConfig) as err: - raise vol.Invalid("invalid condition configuration: " + str(err)) from err + except InvalidDeviceAutomationConfig as err: + raise vol.Invalid(str(err) or "Invalid condition configuration") from err async def async_condition_from_config( diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index aac56b39846..bd72b24d844 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -58,15 +58,14 @@ async def async_validate_trigger_config( ) -> ConfigType: """Validate config.""" try: - config = TRIGGER_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER ) if not hasattr(platform, "async_validate_trigger_config"): return cast(ConfigType, platform.TRIGGER_SCHEMA(config)) return await platform.async_validate_trigger_config(hass, config) - except (vol.Invalid, InvalidDeviceAutomationConfig) as err: - raise InvalidDeviceAutomationConfig("invalid trigger configuration") from err + except InvalidDeviceAutomationConfig as err: + raise vol.Invalid(str(err) or "Invalid trigger configuration") from err async def async_attach_trigger( diff --git a/homeassistant/components/rfxtrx/device_action.py b/homeassistant/components/rfxtrx/device_action.py index 7ea4ed07423..15595b88cd2 100644 --- a/homeassistant/components/rfxtrx/device_action.py +++ b/homeassistant/components/rfxtrx/device_action.py @@ -80,6 +80,7 @@ async def async_validate_action_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" + config = ACTION_SCHEMA(config) commands, _ = _get_commands(hass, config[CONF_DEVICE_ID], config[CONF_TYPE]) sub_type = config[CONF_SUBTYPE] diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 71c062cf7d9..3ead6fcb35d 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -720,28 +720,7 @@ async def test_automation_with_bad_condition_action(hass, caplog): assert "required key not provided" in caplog.text -async def test_automation_with_bad_condition_missing_domain(hass, caplog): - """Test automation with bad device condition.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "hello", - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": {"condition": "device", "device_id": "hello.device"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, - } - }, - ) - - assert ( - "Invalid config for [automation]: required key not provided @ data['condition'][0]['domain']" - in caplog.text - ) - - -async def test_automation_with_bad_condition_missing_device_id(hass, caplog): +async def test_automation_with_bad_condition(hass, caplog): """Test automation with bad device condition.""" assert await async_setup_component( hass, @@ -756,10 +735,7 @@ async def test_automation_with_bad_condition_missing_device_id(hass, caplog): }, ) - assert ( - "Invalid config for [automation]: required key not provided @ data['condition'][0]['device_id']" - in caplog.text - ) + assert "required key not provided" in caplog.text @pytest.fixture @@ -900,25 +876,8 @@ async def test_automation_with_bad_sub_condition(hass, caplog): assert "required key not provided" in caplog.text -async def test_automation_with_bad_trigger_missing_domain(hass, caplog): - """Test automation with device trigger this is missing domain.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "hello", - "trigger": {"platform": "device", "device_id": "hello.device"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, - } - }, - ) - - assert "required key not provided @ data['domain']" in caplog.text - - -async def test_automation_with_bad_trigger_missing_device_id(hass, caplog): - """Test automation with device trigger that is missing device_id.""" +async def test_automation_with_bad_trigger(hass, caplog): + """Test automation with bad device trigger.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -931,7 +890,7 @@ async def test_automation_with_bad_trigger_missing_device_id(hass, caplog): }, ) - assert "required key not provided @ data['device_id']" in caplog.text + assert "required key not provided" in caplog.text async def test_websocket_device_not_found(hass, hass_ws_client): diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index 96914885971..db15ce3a592 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -128,7 +128,8 @@ async def test_get_triggers_for_invalid_device_id(hass, caplog): await hass.async_block_till_done() assert ( - "Invalid config for [automation]: invalid trigger configuration" in caplog.text + "Invalid config for [automation]: Device invalid_device_id is not a valid webostv device" + in caplog.text ) From 410479b21274e82aa47c89400af23befb53dafd4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Oct 2022 16:03:24 +0200 Subject: [PATCH 132/183] Update pyoverkiz to 1.5.5 (#79798) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 0b3e041f302..480b0b1d9ed 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.5.3"], + "requirements": ["pyoverkiz==1.5.5"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 32ea83cc4d5..5ae33ea76fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1783,7 +1783,7 @@ pyotgw==2.0.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.3 +pyoverkiz==1.5.5 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e514bec018..565697ec62a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1260,7 +1260,7 @@ pyotgw==2.0.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.3 +pyoverkiz==1.5.5 # homeassistant.components.openweathermap pyowm==3.2.0 From b182305ff660e017dd22aa2d4fe90759f80d3733 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sat, 8 Oct 2022 01:47:24 +0200 Subject: [PATCH 133/183] Fix realtime option for hvv_departures (#79799) --- homeassistant/components/hvv_departures/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 0a516529386..e2de1758dd5 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle from homeassistant.util.dt import get_time_zone, utcnow -from .const import ATTRIBUTION, CONF_STATION, DOMAIN, MANUFACTURER +from .const import ATTRIBUTION, CONF_REAL_TIME, CONF_STATION, DOMAIN, MANUFACTURER MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) MAX_LIST = 20 @@ -90,7 +90,7 @@ class HVVDepartureSensor(SensorEntity): }, "maxList": MAX_LIST, "maxTimeOffset": MAX_TIME_OFFSET, - "useRealtime": self.config_entry.options.get("realtime", False), + "useRealtime": self.config_entry.options.get(CONF_REAL_TIME, False), } if "filter" in self.config_entry.options: From f5368cc3599301e4477c6781a818b8efb91624b9 Mon Sep 17 00:00:00 2001 From: spycle <48740594+spycle@users.noreply.github.com> Date: Sat, 8 Oct 2022 07:19:40 +0100 Subject: [PATCH 134/183] Fix keymitt_ble discovery (#79809) * Fix keymitt_ble discovery * Update tests * Up version * Up version keymitt_ble * Up version keymitt_ble --- homeassistant/components/keymitt_ble/manifest.json | 7 ++----- homeassistant/generated/bluetooth.py | 6 +----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/keymitt_ble/__init__.py | 4 ++-- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/keymitt_ble/manifest.json b/homeassistant/components/keymitt_ble/manifest.json index 445a2581bda..2a21074bb12 100644 --- a/homeassistant/components/keymitt_ble/manifest.json +++ b/homeassistant/components/keymitt_ble/manifest.json @@ -5,17 +5,14 @@ "config_flow": true, "bluetooth": [ { - "service_uuid": "00001831-0000-1000-8000-00805f9b34fb" - }, - { - "service_data_uuid": "00001831-0000-1000-8000-00805f9b34fb" + "service_uuid": "0000abcd-0000-1000-8000-00805f9b34fb" }, { "local_name": "mib*" } ], "codeowners": ["@spycle"], - "requirements": ["PyMicroBot==0.0.6"], + "requirements": ["PyMicroBot==0.0.8"], "iot_class": "assumed_state", "dependencies": ["bluetooth"], "loggers": ["keymitt_ble"] diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 8975ac66611..181a4034bf0 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -173,11 +173,7 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ }, { "domain": "keymitt_ble", - "service_uuid": "00001831-0000-1000-8000-00805f9b34fb", - }, - { - "domain": "keymitt_ble", - "service_data_uuid": "00001831-0000-1000-8000-00805f9b34fb", + "service_uuid": "0000abcd-0000-1000-8000-00805f9b34fb", }, { "domain": "keymitt_ble", diff --git a/requirements_all.txt b/requirements_all.txt index 5ae33ea76fa..9883438bff3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -23,7 +23,7 @@ PyFlick==0.0.2 PyMVGLive==1.1.4 # homeassistant.components.keymitt_ble -PyMicroBot==0.0.6 +PyMicroBot==0.0.8 # homeassistant.components.mobile_app # homeassistant.components.owntracks diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 565697ec62a..6717836b42b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ HAP-python==4.5.0 PyFlick==0.0.2 # homeassistant.components.keymitt_ble -PyMicroBot==0.0.6 +PyMicroBot==0.0.8 # homeassistant.components.mobile_app # homeassistant.components.owntracks diff --git a/tests/components/keymitt_ble/__init__.py b/tests/components/keymitt_ble/__init__.py index 0b145970643..7ae4c20c406 100644 --- a/tests/components/keymitt_ble/__init__.py +++ b/tests/components/keymitt_ble/__init__.py @@ -32,7 +32,7 @@ def patch_async_setup_entry(return_value=True): SERVICE_INFO = BluetoothServiceInfoBleak( name="mibp", - service_uuids=["00001831-0000-1000-8000-00805f9b34fb"], + service_uuids=["0000abcd-0000-1000-8000-00805f9b34fb"], address="aa:bb:cc:dd:ee:ff", manufacturer_data={}, service_data={}, @@ -41,7 +41,7 @@ SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=AdvertisementData( local_name="mibp", manufacturer_data={}, - service_uuids=["00001831-0000-1000-8000-00805f9b34fb"], + service_uuids=["0000abcd-0000-1000-8000-00805f9b34fb"], ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "mibp"), time=0, From c64b286f6e54ad029c1840e56495fa922e5ce563 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 8 Oct 2022 08:49:24 +0200 Subject: [PATCH 135/183] Fix POE control port_idx error in UniFi (#79838) Bump UniFi dependency --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 6bf9f8aa473..eeb974242e9 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==37"], + "requirements": ["aiounifi==38"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9883438bff3..23c9bf191ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==37 +aiounifi==38 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6717836b42b..5c9b3ab7fb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -251,7 +251,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==37 +aiounifi==38 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 5812d802ee00e5bd2ae75a9dcc17746da7f7ded9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 8 Oct 2022 17:32:46 +0200 Subject: [PATCH 136/183] Update typing-extensions constraint to >=4.4.0 (#79860) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f493034171f..92b535f0d6f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ pyyaml==6.0 requests==2.28.1 scapy==2.4.5 sqlalchemy==1.4.41 -typing-extensions>=3.10.0.2,<5.0 +typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.8.1 diff --git a/pyproject.toml b/pyproject.toml index 287e1c6d627..a2399b29a67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "python-slugify==4.0.1", "pyyaml==6.0", "requests==2.28.1", - "typing-extensions>=3.10.0.2,<5.0", + "typing-extensions>=4.4.0,<5.0", "voluptuous==0.13.1", "voluptuous-serialize==2.5.0", "yarl==1.8.1", diff --git a/requirements.txt b/requirements.txt index 28d3c11081b..0dfc353823a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ pip>=21.0,<22.3 python-slugify==4.0.1 pyyaml==6.0 requests==2.28.1 -typing-extensions>=3.10.0.2,<5.0 +typing-extensions>=4.4.0,<5.0 voluptuous==0.13.1 voluptuous-serialize==2.5.0 yarl==1.8.1 From 5ac258378bbe93a31d50560adcd8b7c49714f1f1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 8 Oct 2022 15:40:25 -0400 Subject: [PATCH 137/183] Bump ZHA dependencies (#79898) --- homeassistant/components/zha/manifest.json | 10 +++++----- requirements_all.txt | 10 +++++----- requirements_test_all.txt | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 803a7daabbe..426ac24bbe3 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,15 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.34.1", + "bellows==0.34.2", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.82", "zigpy-deconz==0.19.0", - "zigpy==0.51.2", - "zigpy-xbee==0.16.0", - "zigpy-zigate==0.10.0", - "zigpy-znp==0.9.0" + "zigpy==0.51.3", + "zigpy-xbee==0.16.1", + "zigpy-zigate==0.10.1", + "zigpy-znp==0.9.1" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 23c9bf191ff..8cb538d51b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -401,7 +401,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.1 +bellows==0.34.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -2604,16 +2604,16 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.16.0 +zigpy-xbee==0.16.1 # homeassistant.components.zha -zigpy-zigate==0.10.0 +zigpy-zigate==0.10.1 # homeassistant.components.zha -zigpy-znp==0.9.0 +zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.2 +zigpy==0.51.3 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c9b3ab7fb2..741fdf10341 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.1 +bellows==0.34.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -1799,16 +1799,16 @@ zha-quirks==0.0.82 zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.16.0 +zigpy-xbee==0.16.1 # homeassistant.components.zha -zigpy-zigate==0.10.0 +zigpy-zigate==0.10.1 # homeassistant.components.zha -zigpy-znp==0.9.0 +zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.2 +zigpy==0.51.3 # homeassistant.components.zwave_js zwave-js-server-python==0.43.0 From f330745cdb9ba112f868767d90c4e0b38470ea14 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 9 Oct 2022 01:30:48 +0200 Subject: [PATCH 138/183] Bump pyatmo to 7.1.1 (#79918) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 74d34056241..1e3354f1c27 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.1.0"], + "requirements": ["pyatmo==7.1.1"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index 8cb538d51b8..921f36cc597 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1436,7 +1436,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.1.0 +pyatmo==7.1.1 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 741fdf10341..8112bef68b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1024,7 +1024,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.1.0 +pyatmo==7.1.1 # homeassistant.components.apple_tv pyatv==0.10.3 From bd2ac57d55ed0ffaf9437fb5748dfebf668d2822 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 9 Oct 2022 15:02:37 -0400 Subject: [PATCH 139/183] Bumped version to 2022.10.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cb7eb689b2d..588cd972966 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 = 10 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index a2399b29a67..cb32075133e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.1" +version = "2022.10.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 44e201ae2fb03ba809fee1e3024720ac02eace89 Mon Sep 17 00:00:00 2001 From: Khole Date: Mon, 10 Oct 2022 18:54:31 +0100 Subject: [PATCH 140/183] Bump pyhiveapi to 0.5.14 (#79530) --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 406b32d86f8..b7e5b3fa9ea 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -6,7 +6,7 @@ "models": ["HHKBridge*"] }, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.13"], + "requirements": ["pyhiveapi==0.5.14"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 921f36cc597..7af778888f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1601,7 +1601,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.13 +pyhiveapi==0.5.14 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8112bef68b0..f3b7dd7d089 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1123,7 +1123,7 @@ pyhaversion==22.8.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.13 +pyhiveapi==0.5.14 # homeassistant.components.homematic pyhomematic==0.1.77 From 6f31e60d0fe71f46714f4c7b2b5983764b130019 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Oct 2022 17:56:11 -1000 Subject: [PATCH 141/183] Bump bluetooth-auto-recovery to 0.3.4 (#79971) --- 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 f81e1324da4..783d4ce7df2 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -9,7 +9,7 @@ "bleak==0.18.1", "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", - "bluetooth-auto-recovery==0.3.3", + "bluetooth-auto-recovery==0.3.4", "dbus-fast==1.24.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 92b535f0d6f..88a88f07c21 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ bcrypt==3.1.7 bleak-retry-connector==2.1.3 bleak==0.18.1 bluetooth-adapters==0.6.0 -bluetooth-auto-recovery==0.3.3 +bluetooth-auto-recovery==0.3.4 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 7af778888f0..b1c7e64d513 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,7 +438,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.3 +bluetooth-auto-recovery==0.3.4 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3b7dd7d089..f8b4f0b8cd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.3 +bluetooth-auto-recovery==0.3.4 # homeassistant.components.bond bond-async==0.1.22 From b69db7570592b63fb1c784bae336bc9cefa282bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Oct 2022 10:10:28 -1000 Subject: [PATCH 142/183] Bump pySwitchbot to 0.19.15 (#79972) --- 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 1d245d3fd81..282bf6aa447 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.19.13"], + "requirements": ["PySwitchbot==0.19.15"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index b1c7e64d513..2589f23a173 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.13 +PySwitchbot==0.19.15 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8b4f0b8cd2..76e97bdec7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.13 +PySwitchbot==0.19.15 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 86381e74570dc2e0f9e62b2ce9416ab7e8fa89cc Mon Sep 17 00:00:00 2001 From: Michael Molisani Date: Mon, 10 Oct 2022 04:37:02 -0700 Subject: [PATCH 143/183] Update to pygtfs 0.1.7 (#79975) --- homeassistant/components/gtfs/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index 8dfb37ad551..9e9eb6a5585 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -2,7 +2,7 @@ "domain": "gtfs", "name": "General Transit Feed Specification (GTFS)", "documentation": "https://www.home-assistant.io/integrations/gtfs", - "requirements": ["pygtfs==0.1.6"], + "requirements": ["pygtfs==0.1.7"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pygtfs"] diff --git a/requirements_all.txt b/requirements_all.txt index 2589f23a173..78ab43ee33d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1586,7 +1586,7 @@ pyfttt==0.3 pygatt[GATTTOOL]==4.0.5 # homeassistant.components.gtfs -pygtfs==0.1.6 +pygtfs==0.1.7 # homeassistant.components.hvv_departures pygti==0.9.3 From 18caf0ad7dc76f250399329b38a30d4335dd59c7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 10 Oct 2022 21:40:17 +0200 Subject: [PATCH 144/183] Update frontend to 20221010.0 (#79994) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6f243da444a..98b978964a6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221006.0"], + "requirements": ["home-assistant-frontend==20221010.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 88a88f07c21..183a0205762 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.24.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221006.0 +home-assistant-frontend==20221010.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 78ab43ee33d..515201988c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,7 +865,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221006.0 +home-assistant-frontend==20221010.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76e97bdec7d..0fb711846ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -645,7 +645,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221006.0 +home-assistant-frontend==20221010.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 06bb3e8f12bc7da540d7035169a5c1a018434607 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Oct 2022 11:05:28 +0200 Subject: [PATCH 145/183] Remove system marker from Supervisor integration (#79997) --- homeassistant/components/hassio/manifest.json | 3 +-- homeassistant/generated/integrations.json | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index b087eb25807..5de80fdbd19 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -6,6 +6,5 @@ "after_dependencies": ["panel_custom"], "codeowners": ["@home-assistant/supervisor"], "iot_class": "local_polling", - "quality_scale": "internal", - "integration_type": "system" + "quality_scale": "internal" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index cccbd162e57..4fd58cd88f3 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1690,6 +1690,11 @@ "iot_class": "local_polling", "name": "Harman Kardon AVR" }, + "hassio": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Home Assistant Supervisor" + }, "haveibeenpwned": { "config_flow": false, "iot_class": "cloud_polling", From a6a0741c1036b8e645b9307b41aeceb7971b1381 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 10 Oct 2022 19:58:20 +0100 Subject: [PATCH 146/183] Fix Eve Thermo always showing as heating in homekit_controller even when off (#80019) --- .../components/homekit_controller/climate.py | 8 +++++++ .../homekit_controller/test_climate.py | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 2c4d2e3871d..41788eb4cb2 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -565,6 +565,14 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity): # This characteristic describes the current mode of a device, # e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit. # Can be 0 - 2 (Off, Heat, Cool) + + # If the HVAC is switched off, it must be idle + # This works around a bug in some devices (like Eve radiator valves) that + # return they are heating when they are not. + target = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if target == HeatingCoolingTargetValues.OFF: + return HVACAction.IDLE + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT) return CURRENT_MODE_HOMEKIT_TO_HASS.get(value) diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index c5cad7015d8..0f669c9c51f 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -619,6 +619,27 @@ async def test_hvac_mode_vs_hvac_action(hass, utcnow): assert state.attributes["hvac_action"] == "heating" +async def test_hvac_mode_vs_hvac_action_current_mode_wrong(hass, utcnow): + """Check that we cope with buggy HEATING_COOLING_CURRENT.""" + helper = await setup_test_component(hass, create_thermostat_service) + + await helper.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 22, + CharacteristicsTypes.TEMPERATURE_TARGET: 21, + CharacteristicsTypes.HEATING_COOLING_CURRENT: 1, + CharacteristicsTypes.HEATING_COOLING_TARGET: 0, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: 50, + CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET: 45, + }, + ) + + state = await helper.poll_and_get_state() + assert state.state == "off" + assert state.attributes["hvac_action"] == "idle" + + def create_heater_cooler_service(accessory): """Define thermostat characteristics.""" service = accessory.add_service(ServicesTypes.HEATER_COOLER) From d9f4798dddea023064a05b33e9c0d0973e454a89 Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 10 Oct 2022 19:14:43 +0200 Subject: [PATCH 147/183] Fix armed extra state attribute in fibaro entity (#80034) --- homeassistant/components/fibaro/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 9c2d252d77f..08ee4658107 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -650,8 +650,8 @@ class FibaroDevice(Entity): attr[ATTR_BATTERY_LEVEL] = int( self.fibaro_device.properties.batteryLevel ) - if "fibaroAlarmArm" in self.fibaro_device.interfaces: - attr[ATTR_ARMED] = bool(self.fibaro_device.properties.armed) + if "armed" in self.fibaro_device.properties: + attr[ATTR_ARMED] = self.fibaro_device.properties.armed.lower() == "true" except (ValueError, KeyError): pass From 2cdd45fe6f6639f5c3b90f3bb8249b6177d70988 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 10 Oct 2022 22:27:21 +0200 Subject: [PATCH 148/183] Bump aiounifi to v39 (#80043) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index eeb974242e9..365ce086fb0 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==38"], + "requirements": ["aiounifi==39"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 515201988c1..bdf6ed9bfb6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==38 +aiounifi==39 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fb711846ef..35875c2682b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -251,7 +251,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==38 +aiounifi==39 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From b7ff337a51a03ece0e6b1f0f6a8fe1dae5acf0a5 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 10 Oct 2022 22:04:41 +0200 Subject: [PATCH 149/183] Fix Netatmo device trigger (#80047) --- .../components/netatmo/device_trigger.py | 8 +++---- .../components/netatmo/test_device_trigger.py | 22 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index b037f45533f..c6a519a37d0 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -38,10 +38,10 @@ from .const import ( CONF_SUBTYPE = "subtype" DEVICES = { - "NACamera": INDOOR_CAMERA_TRIGGERS, - "NOC": OUTDOOR_CAMERA_TRIGGERS, - "NATherm1": CLIMATE_TRIGGERS, - "NRV": CLIMATE_TRIGGERS, + "Smart Indoor Camera": INDOOR_CAMERA_TRIGGERS, + "Smart Outdoor Camera": OUTDOOR_CAMERA_TRIGGERS, + "Smart Thermostat": CLIMATE_TRIGGERS, + "Smart Valve": CLIMATE_TRIGGERS, } SUBTYPES = { diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index 25b86f8410e..f7a3772b404 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -47,10 +47,10 @@ def calls(hass): @pytest.mark.parametrize( "platform,device_type,event_types", [ - ("camera", "NOC", OUTDOOR_CAMERA_TRIGGERS), - ("camera", "NACamera", INDOOR_CAMERA_TRIGGERS), - ("climate", "NRV", CLIMATE_TRIGGERS), - ("climate", "NATherm1", CLIMATE_TRIGGERS), + ("camera", "Smart Outdoor Camera", OUTDOOR_CAMERA_TRIGGERS), + ("camera", "Smart Indoor Camera", INDOOR_CAMERA_TRIGGERS), + ("climate", "Smart Valve", CLIMATE_TRIGGERS), + ("climate", "Smart Thermostat", CLIMATE_TRIGGERS), ], ) async def test_get_triggers( @@ -105,15 +105,15 @@ async def test_get_triggers( @pytest.mark.parametrize( "platform,camera_type,event_type", - [("camera", "NOC", trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS] - + [("camera", "NACamera", trigger) for trigger in INDOOR_CAMERA_TRIGGERS] + [("camera", "Smart Outdoor Camera", trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS] + + [("camera", "Smart Indoor Camera", trigger) for trigger in INDOOR_CAMERA_TRIGGERS] + [ - ("climate", "NRV", trigger) + ("climate", "Smart Valve", trigger) for trigger in CLIMATE_TRIGGERS if trigger not in SUBTYPES ] + [ - ("climate", "NATherm1", trigger) + ("climate", "Smart Thermostat", trigger) for trigger in CLIMATE_TRIGGERS if trigger not in SUBTYPES ], @@ -183,12 +183,12 @@ async def test_if_fires_on_event( @pytest.mark.parametrize( "platform,camera_type,event_type,sub_type", [ - ("climate", "NRV", trigger, subtype) + ("climate", "Smart Valve", trigger, subtype) for trigger in SUBTYPES for subtype in SUBTYPES[trigger] ] + [ - ("climate", "NATherm1", trigger, subtype) + ("climate", "Smart Thermostat", trigger, subtype) for trigger in SUBTYPES for subtype in SUBTYPES[trigger] ], @@ -262,7 +262,7 @@ async def test_if_fires_on_event_with_subtype( @pytest.mark.parametrize( "platform,device_type,event_type", - [("climate", "NAPLUG", trigger) for trigger in CLIMATE_TRIGGERS], + [("climate", "NAPlug", trigger) for trigger in CLIMATE_TRIGGERS], ) async def test_if_invalid_device( hass, device_reg, entity_reg, platform, device_type, event_type From 974925ebc937d912443eac285709cc0290c6ba42 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:43:31 -0400 Subject: [PATCH 150/183] Bump ZHA dependencies (#80049) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 426ac24bbe3..7067491a12a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -10,8 +10,8 @@ "zha-quirks==0.0.82", "zigpy-deconz==0.19.0", "zigpy==0.51.3", - "zigpy-xbee==0.16.1", - "zigpy-zigate==0.10.1", + "zigpy-xbee==0.16.2", + "zigpy-zigate==0.10.2", "zigpy-znp==0.9.1" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index bdf6ed9bfb6..857a8a0395a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2604,10 +2604,10 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.16.1 +zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.1 +zigpy-zigate==0.10.2 # homeassistant.components.zha zigpy-znp==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35875c2682b..49b17f021c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1799,10 +1799,10 @@ zha-quirks==0.0.82 zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.16.1 +zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.1 +zigpy-zigate==0.10.2 # homeassistant.components.zha zigpy-znp==0.9.1 From d969bb3baee593a1c5f9c2c284d3182fe1e62277 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Oct 2022 22:48:39 +0200 Subject: [PATCH 151/183] Bumped version to 2022.10.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 588cd972966..012a633947d 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 = 10 -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 cb32075133e..8b6d4707521 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.2" +version = "2022.10.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d79ed3acbbc2f24f7e3644682f27aaf8218b9376 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Oct 2022 10:49:54 +0200 Subject: [PATCH 152/183] Fix state saving when sharing topics for MQTT entities (#79421) * Do not write old state sharing availability topic * Add a test * Support for all availability topics * delay async_write_ha_state till last callback * Process write req after processing callback jobs * Do not count subscription callbacks * Simplify * Stale docsting * No topic needed for delays state write * No need to clear when reloading * Move test to test_mixins.py * Only set up sensor platform for test --- .../components/mqtt/alarm_control_panel.py | 4 +- .../components/mqtt/binary_sensor.py | 3 +- homeassistant/components/mqtt/client.py | 1 + homeassistant/components/mqtt/climate.py | 14 ++-- homeassistant/components/mqtt/cover.py | 6 +- .../mqtt/device_tracker/schema_discovery.py | 3 +- homeassistant/components/mqtt/fan.py | 12 +-- homeassistant/components/mqtt/humidifier.py | 12 +-- .../components/mqtt/light/schema_basic.py | 22 +++--- .../components/mqtt/light/schema_json.py | 4 +- .../components/mqtt/light/schema_template.py | 3 +- homeassistant/components/mqtt/lock.py | 3 +- homeassistant/components/mqtt/mixins.py | 6 +- homeassistant/components/mqtt/models.py | 21 ++++++ homeassistant/components/mqtt/number.py | 3 +- homeassistant/components/mqtt/select.py | 3 +- homeassistant/components/mqtt/sensor.py | 6 +- homeassistant/components/mqtt/siren.py | 3 +- homeassistant/components/mqtt/subscription.py | 7 +- homeassistant/components/mqtt/switch.py | 3 +- .../components/mqtt/vacuum/schema_legacy.py | 4 +- .../components/mqtt/vacuum/schema_state.py | 4 +- tests/components/mqtt/test_mixins.py | 73 +++++++++++++++++++ 23 files changed, 164 insertions(+), 56 deletions(-) create mode 100644 tests/components/mqtt/test_mixins.py diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c3502cd8e64..ed1990d919e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -49,7 +49,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -211,7 +211,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): _LOGGER.warning("Received unexpected payload: %s", msg.payload) return self._state = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index f0e5ecc9df8..915a2780283 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -47,6 +47,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -260,7 +261,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): self.hass, off_delay, off_delay_listener ) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index bd734318938..79d720338be 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -658,6 +658,7 @@ class MQTT: timestamp, ), ) + self._mqtt_data.state_write_requests.process_write_state_requests() def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: """Publish / Subscribe / Unsubscribe callback.""" diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 96c7ca3665b..9f98fcfebdc 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -55,7 +55,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -494,7 +494,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload, ) return - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) @@ -505,7 +505,7 @@ class MqttClimate(MqttEntity, ClimateEntity): try: setattr(self, attr, float(payload)) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.error("Could not parse temperature from %s", payload) @@ -564,7 +564,7 @@ class MqttClimate(MqttEntity, ClimateEntity): _LOGGER.error("Invalid %s mode: %s", mode_list, payload) else: setattr(self, attr, payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -623,7 +623,7 @@ class MqttClimate(MqttEntity, ClimateEntity): else: _LOGGER.error("Invalid %s mode: %s", attr, payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -640,7 +640,7 @@ class MqttClimate(MqttEntity, ClimateEntity): preset_mode = render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE) if preset_mode in [PRESET_NONE, PAYLOAD_NONE]: self._preset_mode = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not preset_mode: _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) @@ -654,7 +654,7 @@ class MqttClimate(MqttEntity, ClimateEntity): ) else: self._preset_mode = preset_mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_subscription( topics, CONF_PRESET_MODE_STATE_TOPIC, handle_preset_mode_received diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 1f5d26c3a78..6ed12b8adef 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -51,7 +51,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -405,7 +405,7 @@ class MqttCover(MqttEntity, CoverEntity): ) return - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -451,7 +451,7 @@ class MqttCover(MqttEntity, CoverEntity): else STATE_OPEN ) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_GET_POSITION_TOPIC): topics["get_position_topic"] = { diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 907d424e8a4..1d3f9d109f6 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -29,6 +29,7 @@ from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper from ..models import MqttValueTemplate +from ..util import get_mqtt_data CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" @@ -106,7 +107,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): else: self._location_name = msg.payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 584df08e7d7..866b429c68f 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -55,7 +55,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" @@ -391,7 +391,7 @@ class MqttFan(MqttEntity, FanEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -413,7 +413,7 @@ class MqttFan(MqttEntity, FanEntity): return if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]: self._percentage = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: percentage = ranged_value_to_percentage( @@ -436,7 +436,7 @@ class MqttFan(MqttEntity, FanEntity): ) return self._percentage = percentage - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PERCENTAGE_STATE_TOPIC] is not None: topics[CONF_PERCENTAGE_STATE_TOPIC] = { @@ -469,7 +469,7 @@ class MqttFan(MqttEntity, FanEntity): return self._preset_mode = preset_mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None: topics[CONF_PRESET_MODE_STATE_TOPIC] = { @@ -492,7 +492,7 @@ class MqttFan(MqttEntity, FanEntity): self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: self._oscillation = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: topics[CONF_OSCILLATION_STATE_TOPIC] = { diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 837bbb8b909..7514f0ff672 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -51,7 +51,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_AVAILABLE_MODES_LIST = "modes" CONF_DEVICE_CLASS = "device_class" @@ -309,7 +309,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -331,7 +331,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): return if rendered_target_humidity_payload == self._payload["HUMIDITY_RESET"]: self._target_humidity = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: target_humidity = round(float(rendered_target_humidity_payload)) @@ -355,7 +355,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) return self._target_humidity = target_humidity - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is not None: topics[CONF_TARGET_HUMIDITY_STATE_TOPIC] = { @@ -373,7 +373,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): mode = self._value_templates[ATTR_MODE](msg.payload) if mode == self._payload["MODE_RESET"]: self._mode = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not mode: _LOGGER.debug("Ignoring empty mode from '%s'", msg.topic) @@ -388,7 +388,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): return self._mode = mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_MODE_STATE_TOPIC] is not None: topics[CONF_MODE_STATE_TOPIC] = { diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index e2805781f45..d435d4e91ad 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -51,7 +51,7 @@ from ..const import ( from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..models import MqttCommandTemplate, MqttValueTemplate -from ..util import valid_publish_topic, valid_subscribe_topic +from ..util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -438,7 +438,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -462,7 +462,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): device_value = float(payload) percent_bright = device_value / self._config[CONF_BRIGHTNESS_SCALE] self._brightness = percent_bright * 255 - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) @@ -493,7 +493,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgb: return self._rgb_color = rgb - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGB_STATE_TOPIC, rgb_received) @@ -510,7 +510,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgbw: return self._rgbw_color = rgbw - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) @@ -527,7 +527,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgbww: return self._rgbww_color = rgbww - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) @@ -543,7 +543,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return self._color_mode = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) @@ -561,7 +561,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.COLOR_TEMP self._color_temp = int(payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) @@ -577,7 +577,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return self._effect = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) @@ -594,7 +594,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.HS self._hs_color = hs_color - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.debug("Failed to parse hs state update: '%s'", payload) @@ -613,7 +613,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.XY self._xy_color = xy_color - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_XY_STATE_TOPIC, xy_received) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 8843e8542eb..a4a76673176 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -58,7 +58,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity -from ..util import valid_subscribe_topic +from ..util import get_mqtt_data, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import ( CONF_BRIGHTNESS_SCALE, @@ -401,7 +401,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): with suppress(KeyError): self._effect = values["effect"] - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index dacc977a036..33c7f1cea1b 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -41,6 +41,7 @@ from ..const import ( from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..models import MqttValueTemplate +from ..util import get_mqtt_data from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -256,7 +257,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): else: _LOGGER.warning("Unsupported effect value received") - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topics[CONF_STATE_TOPIC] is not None: self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index dca02f909dc..c9bdd696896 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -33,6 +33,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" @@ -158,7 +159,7 @@ class MqttLock(MqttEntity, LockEntity): elif payload == self._config[CONF_STATE_UNLOCKED]: self._state = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8022a6e91ae..b5c870a196e 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -435,7 +435,9 @@ class MqttAttributes(Entity): and k not in self._attributes_extra_blocked } self._attributes = filtered_dict - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request( + self + ) else: _LOGGER.warning("JSON result was not a dictionary") self._attributes = None @@ -547,7 +549,7 @@ class MqttAvailability(Entity): self._available[topic] = False self._available_latest = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._available = { topic: (self._available[topic] if topic in self._available else False) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index b7cb81b2ea4..f2f30419b4c 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -236,6 +236,26 @@ class MqttValueTemplate: ) +class EntityTopicState: + """Manage entity state write requests for subscribed topics.""" + + def __init__(self) -> None: + """Register topic.""" + self.subscribe_calls: dict[str, Entity] = {} + + @callback + def process_write_state_requests(self) -> None: + """Process the write state requests.""" + while self.subscribe_calls: + _, entity = self.subscribe_calls.popitem() + entity.async_write_ha_state() + + @callback + def write_state_request(self, entity: Entity) -> None: + """Register write state request.""" + self.subscribe_calls[entity.entity_id] = entity + + @dataclass class MqttData: """Keep the MQTT entry data.""" @@ -264,6 +284,7 @@ class MqttData: default_factory=dict ) reload_needed: bool = False + state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) subscriptions_to_restore: list[Subscription] = field(default_factory=list) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) updated_config: ConfigType = field(default_factory=dict) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 09f9d122b98..25ef7af8d6e 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -49,6 +49,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -222,7 +223,7 @@ class MqttNumber(MqttEntity, RestoreNumber): return self._current_number = num_value - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index a6de0495690..12593550e2f 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -35,6 +35,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -169,7 +170,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): return self._attr_current_option = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 7c98fdf51b7..52ba1a7e3c2 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -46,7 +46,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate -from .util import valid_subscribe_topic +from .util import get_mqtt_data, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -300,7 +300,7 @@ class MqttSensor(MqttEntity, RestoreSensor): or self._config[CONF_LAST_RESET_TOPIC] == self._config[CONF_STATE_TOPIC] ): _update_last_reset(msg) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) topics["state_topic"] = { "topic": self._config[CONF_STATE_TOPIC], @@ -314,7 +314,7 @@ class MqttSensor(MqttEntity, RestoreSensor): def last_reset_message_received(msg): """Handle new last_reset messages.""" _update_last_reset(msg) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if ( CONF_LAST_RESET_TOPIC in self._config diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index c8332046092..2ab226e44c0 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -54,6 +54,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" @@ -283,7 +284,7 @@ class MqttSiren(MqttEntity, SirenEntity): ) return self._update(process_turn_on_params(self, json_payload)) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index d0af533f294..05f7f3934ee 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -26,9 +26,12 @@ class EntitySubscription: qos: int = attr.ib(default=0) encoding: str = attr.ib(default="utf-8") - def resubscribe_if_necessary(self, hass, other): + def resubscribe_if_necessary( + self, hass: HomeAssistant, other: EntitySubscription | None + ) -> None: """Re-subscribe to the new topic if necessary.""" if not self._should_resubscribe(other): + assert other self.unsubscribe_callback = other.unsubscribe_callback return @@ -56,7 +59,7 @@ class EntitySubscription: return self.unsubscribe_callback = await self.subscribe_task - def _should_resubscribe(self, other): + def _should_resubscribe(self, other: EntitySubscription | None) -> bool: """Check if we should re-subscribe to the topic using the old state.""" if other is None: return True diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index af16b14bea1..f8bf2f5bc6a 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -47,6 +47,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" @@ -168,7 +169,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 6b957aded5c..09c4448fda7 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -20,7 +20,7 @@ from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema from ..models import MqttValueTemplate -from ..util import valid_publish_topic +from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -320,7 +320,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if fan_speed: self._fan_speed = fan_speed - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) topics_list = {topic for topic in self._state_topics.values() if topic} self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index af6c8d289d8..8dfaba80109 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -32,7 +32,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema -from ..util import valid_publish_topic +from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -211,7 +211,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): ) del payload[STATE] self._state_attrs.update(payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC): topics["state_position_topic"] = { diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py new file mode 100644 index 00000000000..97a959b8dbe --- /dev/null +++ b/tests/components/mqtt/test_mixins.py @@ -0,0 +1,73 @@ +"""The tests for shared code of the MQTT platform.""" + +from unittest.mock import patch + +from homeassistant.components import mqtt, sensor +from homeassistant.const import EVENT_STATE_CHANGED, Platform +import homeassistant.core as ha +from homeassistant.setup import async_setup_component + +from tests.common import async_fire_mqtt_message + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_availability_with_shared_state_topic( + hass, + mqtt_mock_entry_with_yaml_config, +): + """Test the state is not changed twice. + + When an entity with a shared state_topic and availability_topic becomes available + The state should only change once. + """ + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "availability_topic": "test-topic", + "payload_available": True, + "payload_not_available": False, + "value_template": "{{ int(value) or '' }}", + "availability_template": "{{ value != '0' }}", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + events = [] + + @ha.callback + def callback(event): + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message(hass, "test-topic", "100") + await hass.async_block_till_done() + # Initially the state and the availability change + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "50") + await hass.async_block_till_done() + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "0") + await hass.async_block_till_done() + # Only the availability is changed since the template resukts in an empty payload + # This does not change the state + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "10") + await hass.async_block_till_done() + # The availability is changed but the topic is shared, + # hence there the state will be written when the value is updated + assert len(events) == 1 From cf6333463ba4cd446ebbba2b1e51ed392524fa0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Thu, 13 Oct 2022 11:40:47 +0200 Subject: [PATCH 153/183] Fix nobo_hub presenting temperature in zone with one decimal (#79743) Fix presenting temperature in zone with one decimal. Fix stepping the target temperatur without decimals. --- homeassistant/components/nobo_hub/climate.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index a465bfa77ab..ba38e0b1530 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -1,7 +1,6 @@ """Python Control of Nobø Hub - Nobø Energy Control.""" from __future__ import annotations -import logging from typing import Any from pynobo import nobo @@ -24,7 +23,7 @@ from homeassistant.const import ( ATTR_NAME, ATTR_SUGGESTED_AREA, ATTR_VIA_DEVICE, - PRECISION_WHOLE, + PRECISION_TENTHS, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -52,8 +51,6 @@ PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY] MIN_TEMPERATURE = 7 MAX_TEMPERATURE = 40 -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -87,11 +84,12 @@ class NoboZone(ClimateEntity): _attr_max_temp = MAX_TEMPERATURE _attr_min_temp = MIN_TEMPERATURE - _attr_precision = PRECISION_WHOLE + _attr_precision = PRECISION_TENTHS _attr_preset_modes = PRESET_MODES - # Need to poll to get preset change when in HVACMode.AUTO. _attr_supported_features = SUPPORT_FLAGS _attr_temperature_unit = TEMP_CELSIUS + _attr_target_temperature_step = 1 + # Need to poll to get preset change when in HVACMode.AUTO, so can't set _attr_should_poll = False def __init__(self, zone_id, hub: nobo, override_type): """Initialize the climate device.""" From b988c5e591fe4045b825b2f1a3e23959fa8b0065 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 7 Oct 2022 12:17:31 +1300 Subject: [PATCH 154/183] Bump aioesphomeapi to 11.1.1 (#79762) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 066050d796d..3ffceaae971 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.1.0"], + "requirements": ["aioesphomeapi==11.1.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 857a8a0395a..62f6dbb84ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.1.0 +aioesphomeapi==11.1.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49b17f021c1..583e5080d85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.1.0 +aioesphomeapi==11.1.1 # homeassistant.components.flo aioflo==2021.11.0 From 3311a092ea90db208092f9e3a8b7a31631370d10 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Fri, 14 Oct 2022 13:09:00 +0200 Subject: [PATCH 155/183] Fix wallbox jwt issue (#79948) * Bump Wallbox package * remove debug message * Force update of auth token by emptying it first * Force token refresh by emptying token Improve exception handling * include tests * Update __init__.py * Removed the clearing ot jwt token, issue is fixed by upstream fix in wallbox package. * Catch connectionerror * Update homeassistant/components/wallbox/__init__.py Co-authored-by: Martin Hjelmare * Run black Co-authored-by: Martin Hjelmare --- homeassistant/components/wallbox/__init__.py | 11 +++++--- homeassistant/components/wallbox/lock.py | 3 +++ .../components/wallbox/manifest.json | 2 +- homeassistant/components/wallbox/number.py | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wallbox/__init__.py | 26 +++++++++++++++++++ tests/components/wallbox/test_lock.py | 13 ++++++++++ tests/components/wallbox/test_number.py | 19 +++++++++++++- 9 files changed, 73 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 9175f42827d..6382cf05940 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -17,6 +17,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from .const import ( @@ -93,6 +94,7 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Authenticate using Wallbox API.""" try: self._wallbox.authenticate() + except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN: raise ConfigEntryAuthFailed from wallbox_connection_error @@ -125,11 +127,12 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN ) - return data - - except requests.exceptions.HTTPError as wallbox_connection_error: - raise ConnectionError from wallbox_connection_error + except ( + ConnectionError, + requests.exceptions.HTTPError, + ) as wallbox_connection_error: + raise UpdateFailed from wallbox_connection_error async def _async_update_data(self) -> dict[str, Any]: """Get new sensor data for Wallbox component.""" diff --git a/homeassistant/components/wallbox/lock.py b/homeassistant/components/wallbox/lock.py index d1b3c787735..7b5dca58010 100644 --- a/homeassistant/components/wallbox/lock.py +++ b/homeassistant/components/wallbox/lock.py @@ -6,6 +6,7 @@ from typing import Any from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import InvalidAuth, WallboxCoordinator, WallboxEntity @@ -36,6 +37,8 @@ async def async_setup_entry( ) except InvalidAuth: return + except ConnectionError as exc: + raise PlatformNotReady from exc async_add_entities( [ diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 5c195b8bfce..433a759bea5 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -3,7 +3,7 @@ "name": "Wallbox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", - "requirements": ["wallbox==0.4.9"], + "requirements": ["wallbox==0.4.10"], "codeowners": ["@hesselonline"], "iot_class": "cloud_polling", "loggers": ["wallbox"] diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 5470ec11532..04db0ad9f7c 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -7,6 +7,7 @@ from typing import Optional, cast from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import InvalidAuth, WallboxCoordinator, WallboxEntity @@ -46,6 +47,8 @@ async def async_setup_entry( ) except InvalidAuth: return + except ConnectionError as exc: + raise PlatformNotReady from exc async_add_entities( [ diff --git a/requirements_all.txt b/requirements_all.txt index 62f6dbb84ef..d24aaf194af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2503,7 +2503,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.4.9 +wallbox==0.4.10 # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 583e5080d85..4ad895ffa84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1731,7 +1731,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.4.9 +wallbox==0.4.10 # homeassistant.components.folder_watcher watchdog==2.1.9 diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 8c979c42ebe..53af3b6383d 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -173,3 +173,29 @@ async def setup_integration_read_only(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + + +async def setup_integration_platform_not_ready(hass: HomeAssistant) -> None: + """Test wallbox sensor class setup for read only.""" + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, + status_code=HTTPStatus.OK, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + json=test_response, + status_code=HTTPStatus.OK, + ) + mock_request.put( + "https://api.wall-box.com/v2/charger/12345", + json=test_response, + status_code=HTTPStatus.NOT_FOUND, + ) + + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py index 567d92757cd..bf0daa5c828 100644 --- a/tests/components/wallbox/test_lock.py +++ b/tests/components/wallbox/test_lock.py @@ -13,6 +13,7 @@ from . import ( authorisation_response, entry, setup_integration, + setup_integration_platform_not_ready, setup_integration_read_only, ) from .const import MOCK_LOCK_ENTITY_ID @@ -109,3 +110,15 @@ async def test_wallbox_lock_class_authentication_error(hass: HomeAssistant) -> N assert state is None await hass.config_entries.async_unload(entry.entry_id) + + +async def test_wallbox_lock_class_platform_not_ready(hass: HomeAssistant) -> None: + """Test wallbox lock not loaded on authentication error.""" + + await setup_integration_platform_not_ready(hass) + + state = hass.states.get(MOCK_LOCK_ENTITY_ID) + + assert state is None + + await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index 58e3450e6aa..c16c85d3696 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -9,7 +9,12 @@ from homeassistant.components.wallbox import CHARGER_MAX_CHARGING_CURRENT_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from . import authorisation_response, entry, setup_integration +from . import ( + authorisation_response, + entry, + setup_integration, + setup_integration_platform_not_ready, +) from .const import MOCK_NUMBER_ENTITY_ID @@ -71,3 +76,15 @@ async def test_wallbox_number_class_connection_error(hass: HomeAssistant) -> Non blocking=True, ) await hass.config_entries.async_unload(entry.entry_id) + + +async def test_wallbox_number_class_platform_not_ready(hass: HomeAssistant) -> None: + """Test wallbox lock not loaded on authentication error.""" + + await setup_integration_platform_not_ready(hass) + + state = hass.states.get(MOCK_NUMBER_ENTITY_ID) + + assert state is None + + await hass.config_entries.async_unload(entry.entry_id) From 75c2e213b29a1cc9891877826b3aebcca5a53342 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Tue, 11 Oct 2022 08:29:35 -0400 Subject: [PATCH 156/183] Fix audio detection for IP4m-1041 Amcrest camera (#80066) --- homeassistant/components/amcrest/__init__.py | 5 +- .../components/amcrest/binary_sensor.py | 50 +++++++++---------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index e3f48263e8b..8fea717e6bb 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -370,11 +370,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) event_codes = { - sensor.event_code + event_code for sensor in BINARY_SENSORS if sensor.key in binary_sensors and not sensor.should_poll - and sensor.event_code is not None + and sensor.event_codes is not None + for event_code in sensor.event_codes } _start_event_monitor(hass, name, api, event_codes) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index e583aad904b..4d438c7c3bf 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -39,7 +39,7 @@ if TYPE_CHECKING: class AmcrestSensorEntityDescription(BinarySensorEntityDescription): """Describe Amcrest sensor entity.""" - event_code: str | None = None + event_codes: set[str] | None = None should_poll: bool = False @@ -51,7 +51,7 @@ _ONLINE_SCAN_INTERVAL = timedelta(seconds=60 - BINARY_SENSOR_SCAN_INTERVAL_SECS) _AUDIO_DETECTED_KEY = "audio_detected" _AUDIO_DETECTED_POLLED_KEY = "audio_detected_polled" _AUDIO_DETECTED_NAME = "Audio Detected" -_AUDIO_DETECTED_EVENT_CODE = "AudioMutation" +_AUDIO_DETECTED_EVENT_CODES = {"AudioMutation", "AudioIntensity"} _CROSSLINE_DETECTED_KEY = "crossline_detected" _CROSSLINE_DETECTED_POLLED_KEY = "crossline_detected_polled" @@ -70,39 +70,39 @@ BINARY_SENSORS: tuple[AmcrestSensorEntityDescription, ...] = ( key=_AUDIO_DETECTED_KEY, name=_AUDIO_DETECTED_NAME, device_class=BinarySensorDeviceClass.SOUND, - event_code=_AUDIO_DETECTED_EVENT_CODE, + event_codes=_AUDIO_DETECTED_EVENT_CODES, ), AmcrestSensorEntityDescription( key=_AUDIO_DETECTED_POLLED_KEY, name=_AUDIO_DETECTED_NAME, device_class=BinarySensorDeviceClass.SOUND, - event_code=_AUDIO_DETECTED_EVENT_CODE, + event_codes=_AUDIO_DETECTED_EVENT_CODES, should_poll=True, ), AmcrestSensorEntityDescription( key=_CROSSLINE_DETECTED_KEY, name=_CROSSLINE_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_CROSSLINE_DETECTED_EVENT_CODE, + event_codes={_CROSSLINE_DETECTED_EVENT_CODE}, ), AmcrestSensorEntityDescription( key=_CROSSLINE_DETECTED_POLLED_KEY, name=_CROSSLINE_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_CROSSLINE_DETECTED_EVENT_CODE, + event_codes={_CROSSLINE_DETECTED_EVENT_CODE}, should_poll=True, ), AmcrestSensorEntityDescription( key=_MOTION_DETECTED_KEY, name=_MOTION_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_MOTION_DETECTED_EVENT_CODE, + event_codes={_MOTION_DETECTED_EVENT_CODE}, ), AmcrestSensorEntityDescription( key=_MOTION_DETECTED_POLLED_KEY, name=_MOTION_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_MOTION_DETECTED_EVENT_CODE, + event_codes={_MOTION_DETECTED_EVENT_CODE}, should_poll=True, ), AmcrestSensorEntityDescription( @@ -211,13 +211,13 @@ class AmcrestBinarySensor(BinarySensorEntity): log_update_error(_LOGGER, "update", self.name, "binary sensor", error) return - if (event_code := self.entity_description.event_code) is None: - _LOGGER.error("Binary sensor %s event code not set", self.name) - return + if not (event_codes := self.entity_description.event_codes): + raise ValueError(f"Binary sensor {self.name} event codes not set") try: - self._attr_is_on = ( + self._attr_is_on = any( # type: ignore[arg-type] len(await self._api.async_event_channels_happened(event_code)) > 0 + for event_code in event_codes ) except AmcrestError as error: log_update_error(_LOGGER, "update", self.name, "binary sensor", error) @@ -266,17 +266,17 @@ class AmcrestBinarySensor(BinarySensorEntity): ) if ( - self.entity_description.event_code - and not self.entity_description.should_poll - ): - self.async_on_remove( - async_dispatcher_connect( - self.hass, - service_signal( - SERVICE_EVENT, - self._signal_name, - self.entity_description.event_code, - ), - self.async_event_received, + event_codes := self.entity_description.event_codes + ) and not self.entity_description.should_poll: + for event_code in event_codes: + self.async_on_remove( + async_dispatcher_connect( + self.hass, + service_signal( + SERVICE_EVENT, + self._signal_name, + event_code, + ), + self.async_event_received, + ) ) - ) From 5ab97e9f93784125e7be1907c0e343d92b9adba5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Oct 2022 21:21:59 +0200 Subject: [PATCH 157/183] Fix set humidity in Tuya (#80132) --- homeassistant/components/tuya/humidifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 5fd33ba80d0..9891c81a456 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -104,7 +104,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): if int_type := self.find_dpcode( description.humidity, dptype=DPType.INTEGER, prefer_function=True ): - self._set_humiditye = int_type + self._set_humidity = int_type self._attr_min_humidity = int(int_type.min_scaled) self._attr_max_humidity = int(int_type.max_scaled) From b0d2f55f160672e954d0abffeaaa96f62164c6d2 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 12 Oct 2022 16:43:55 +0200 Subject: [PATCH 158/183] Correct units for sensors in nibe heatpump (#80140) --- homeassistant/components/nibe_heatpump/sensor.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index b0bc816dad6..66c66aaabe1 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_HOURS, @@ -72,24 +73,31 @@ UNIT_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, ), + "W": SensorEntityDescription( + key="W", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), "Wh": SensorEntityDescription( key="Wh", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.POWER, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, ), "kWh": SensorEntityDescription( key="kWh", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.POWER, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), "MWh": SensorEntityDescription( key="MWh", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.POWER, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, ), @@ -97,6 +105,7 @@ UNIT_DESCRIPTIONS = { key="h", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=TIME_HOURS, ), } @@ -133,6 +142,7 @@ class Sensor(CoilEntity, SensorEntity): self.entity_description = entity_description else: self._attr_native_unit_of_measurement = coil.unit + self._attr_entity_category = EntityCategory.DIAGNOSTIC def _async_read_coil(self, coil: Coil): self._attr_native_value = coil.value From 3451d150ae68eadebf0d0518f9c43dd41904c18d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Oct 2022 19:30:09 -1000 Subject: [PATCH 159/183] Bump yalexs to 1.2.6 (#80142) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index a816ddc06ff..b7dde070049 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.4"], + "requirements": ["yalexs==1.2.6"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index d24aaf194af..c41973e8bd8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2568,7 +2568,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.4 +yalexs==1.2.6 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ad895ffa84..91437c7afa3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.4 +yalexs==1.2.6 # homeassistant.components.yeelight yeelight==0.7.10 From 8dfd8491a721d682e22f078b8fd81a86344b1873 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 13 Oct 2022 00:06:23 +0200 Subject: [PATCH 160/183] Fix incorrect deprecation year for conversion utils (#80195) Fix incorrect depr year --- homeassistant/util/distance.py | 2 +- homeassistant/util/pressure.py | 2 +- homeassistant/util/speed.py | 2 +- homeassistant/util/temperature.py | 2 +- homeassistant/util/volume.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index f5dbeaf42d5..719379d4c61 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -48,7 +48,7 @@ def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" report( "uses distance utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.DistanceConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 2a8e20ed025..d6d0c79741f 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -27,7 +27,7 @@ def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" report( "uses pressure utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.PressureConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index 76ea873d7fe..f531e2d78f7 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -34,7 +34,7 @@ def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" report( "uses speed utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.SpeedConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 9173fbc5eee..0c2608eb4b5 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -39,7 +39,7 @@ def convert( """Convert a temperature from one unit to another.""" report( "uses temperature utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.TemperatureConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index b468b9e6e0d..e21cebd2982 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -42,7 +42,7 @@ def convert(volume: float, from_unit: str, to_unit: str) -> float: """Convert a volume from one unit to another.""" report( "uses volume utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.VolumeConverter instead", error_if_core=False, ) From a5f9cd2732a28cf31cd8b76243899c5f15229753 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 14:53:09 -1000 Subject: [PATCH 161/183] Fix nexia permanent hold when cool and heat temps are within 2 degrees (#80297) --- homeassistant/components/nexia/climate.py | 4 ++-- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 81e6158a872..66c325d2fc3 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -206,7 +206,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Set the hvac run mode.""" if run_mode is not None: if run_mode == HOLD_PERMANENT: - await self._zone.call_permanent_hold() + await self._zone.set_permanent_hold() else: await self._zone.call_return_to_schedule() if hvac_mode is not None: @@ -399,7 +399,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): await self._zone.call_return_to_schedule() await self._zone.set_mode(mode=OPERATION_MODE_AUTO) else: - await self._zone.call_permanent_hold() + await self._zone.set_permanent_hold() await self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) self._signal_zone_update() diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index e381dc95897..77280b1f503 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.2"], + "requirements": ["nexia==2.0.4"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index e242032c947..643a4d585c4 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -62,7 +62,7 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): if self._zone.get_current_mode() == OPERATION_MODE_OFF: await self._zone.call_permanent_off() else: - await self._zone.call_permanent_hold() + await self._zone.set_permanent_hold() self._signal_zone_update() async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index c41973e8bd8..371ec7e2288 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1132,7 +1132,7 @@ nettigo-air-monitor==1.4.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.2 +nexia==2.0.4 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91437c7afa3..fafbb93568d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -822,7 +822,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.4.2 # homeassistant.components.nexia -nexia==2.0.2 +nexia==2.0.4 # homeassistant.components.discord nextcord==2.0.0a8 From 6f00f42cc6f24a6714cbeae77d193eac2a639c4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 20:51:27 -1000 Subject: [PATCH 162/183] Bump HAP-python to fix pairing with iOS 16 (#80301) Using the ha- fork until upstream can pickup and merge pending PRs. The plan is to revert back to upstream HAP-python when its back in sync Fixes #79305 Fixes #79304 --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 2ca78b6d915..187eaf4c869 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==4.5.0", + "ha-HAP-python==4.5.2", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 371ec7e2288..de488c27520 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -10,9 +10,6 @@ AIOAladdinConnect==0.1.46 # homeassistant.components.adax Adax-local==0.1.4 -# homeassistant.components.homekit -HAP-python==4.5.0 - # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -815,6 +812,9 @@ gstreamer-player==1.1.2 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.homekit +ha-HAP-python==4.5.2 + # homeassistant.components.generic # homeassistant.components.stream ha-av==10.0.0b5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fafbb93568d..93e564bee30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -12,9 +12,6 @@ AIOAladdinConnect==0.1.46 # homeassistant.components.adax Adax-local==0.1.4 -# homeassistant.components.homekit -HAP-python==4.5.0 - # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -607,6 +604,9 @@ gspread==5.5.0 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.homekit +ha-HAP-python==4.5.2 + # homeassistant.components.generic # homeassistant.components.stream ha-av==10.0.0b5 From e6e531e20cbc950c754963214b6685e3073dd9fa Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Fri, 14 Oct 2022 08:11:09 -0700 Subject: [PATCH 163/183] Bump total_connect_client to 2022.10 (#80331) --- homeassistant/components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 461bff0cfd0..71c11958d40 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2022.5"], + "requirements": ["total_connect_client==2022.10"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index de488c27520..e1a1911b95f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2416,7 +2416,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.5 +total_connect_client==2022.10 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93e564bee30..628e8f869cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1656,7 +1656,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.5 +total_connect_client==2022.10 # homeassistant.components.transmission transmissionrpc==0.11 From e8c8a492680891a9bc155af6a0606aee98debed6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Oct 2022 12:04:20 -0400 Subject: [PATCH 164/183] Bumped version to 2022.10.4 --- 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 012a633947d..b60ab0d17e9 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 = 10 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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 8b6d4707521..9a6c1458742 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.3" +version = "2022.10.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 291d36693314fff6ba9c7da853fec1d666de2bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jedelsk=C3=BD?= <8687359+jjedelsky@users.noreply.github.com> Date: Mon, 17 Oct 2022 09:42:20 +0200 Subject: [PATCH 165/183] Handle ReadTimeout during wolflink setup (#78135) * Handle ReadTimeout during wolflink setup * Reorder imports Co-authored-by: Yevhenii Vaskivskyi * Reorder exceptions Co-authored-by: Yevhenii Vaskivskyi * Use RequestError instead of ConnectError, ReadTimeout, and ConnectTimeout Co-authored-by: Yevhenii Vaskivskyi --- homeassistant/components/wolflink/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 9d76c61806b..290e7337617 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from httpx import ConnectError, ConnectTimeout +from httpx import RequestError from wolf_smartset.token_auth import InvalidAuth from wolf_smartset.wolf_client import FetchFailed, ParameterReadError, WolfClient @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for parameter in parameters if parameter.value_id in values } - except ConnectError as exception: + except RequestError as exception: raise UpdateFailed( f"Error communicating with API: {exception}" ) from exception @@ -134,7 +134,7 @@ async def fetch_parameters_init(client: WolfClient, gateway_id: int, device_id: """Fetch all available parameters with usage of WolfClient but handles all exceptions and results in ConfigEntryNotReady.""" try: return await fetch_parameters(client, gateway_id, device_id) - except (ConnectError, ConnectTimeout, FetchFailed) as exception: + except (FetchFailed, RequestError) as exception: raise ConfigEntryNotReady( f"Error communicating with API: {exception}" ) from exception From b3612b430b047d0e7897a7c1dbe1e38558221697 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 7 Oct 2022 20:57:12 +0200 Subject: [PATCH 166/183] Bump plugwise to v0.21.4 (#79831) --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 9c8ea6f3be7..1b17f3e49f5 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.21.3"], + "requirements": ["plugwise==0.21.4"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index e1a1911b95f..69a47eb00d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1306,7 +1306,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.21.3 +plugwise==0.21.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 628e8f869cb..e888858dc5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -933,7 +933,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.21.3 +plugwise==0.21.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 15def407841fc1039c2c0ddf67bd294b9dd35c7d Mon Sep 17 00:00:00 2001 From: Jeef Date: Sat, 15 Oct 2022 14:09:00 -0600 Subject: [PATCH 167/183] Fix Intellifire UDP timeout (#80204) --- homeassistant/components/intellifire/config_flow.py | 4 ++-- homeassistant/components/intellifire/manifest.json | 8 ++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index 4556668b702..d2b019cb381 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -38,7 +38,7 @@ async def validate_host_input(host: str, dhcp_mode: bool = False) -> str: """ LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host) api = IntellifireAPILocal(fireplace_ip=host) - await api.poll(supress_warnings=dhcp_mode) + await api.poll(suppress_warnings=dhcp_mode) serial = api.data.serial LOGGER.debug("Found a fireplace: %s", serial) @@ -62,7 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _find_fireplaces(self): """Perform UDP discovery.""" fireplace_finder = AsyncUDPFireplaceFinder() - discovered_hosts = await fireplace_finder.search_fireplace(timeout=1) + discovered_hosts = await fireplace_finder.search_fireplace(timeout=12) configured_hosts = { entry.data[CONF_HOST] for entry in self._async_current_entries(include_ignore=False) diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index e2ae4bb8abe..67dc5a96ad7 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -3,9 +3,13 @@ "name": "IntelliFire", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/intellifire", - "requirements": ["intellifire4py==2.0.1"], + "requirements": ["intellifire4py==2.2.1"], "codeowners": ["@jeeftor"], "iot_class": "local_polling", "loggers": ["intellifire4py"], - "dhcp": [{ "hostname": "zentrios-*" }] + "dhcp": [ + { + "hostname": "zentrios-*" + } + ] } diff --git a/requirements_all.txt b/requirements_all.txt index 69a47eb00d7..de635d42499 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -934,7 +934,7 @@ inkbird-ble==0.5.5 insteon-frontend-home-assistant==0.2.0 # homeassistant.components.intellifire -intellifire4py==2.0.1 +intellifire4py==2.2.1 # homeassistant.components.iotawatt iotawattpy==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e888858dc5e..ca26b35de93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ inkbird-ble==0.5.5 insteon-frontend-home-assistant==0.2.0 # homeassistant.components.intellifire -intellifire4py==2.0.1 +intellifire4py==2.2.1 # homeassistant.components.iotawatt iotawattpy==0.1.0 From b2034118851c88bd5fe45d02597b5c4f9c367a52 Mon Sep 17 00:00:00 2001 From: rozie Date: Wed, 19 Oct 2022 22:48:39 +0200 Subject: [PATCH 168/183] Fix solaredge missing data value (#80321) * Fix issue #80263: use get to fetch dict value * use None instead -1 for unknown value * Update homeassistant/components/solaredge/coordinator.py Co-authored-by: Franck Nijhof * Add guards for not multipling None * Missing if added Co-authored-by: Franck Nijhof --- homeassistant/components/solaredge/coordinator.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index 7d3e949fc10..5def236598a 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -276,17 +276,19 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService): for key, value in power_flow.items(): if key in ["LOAD", "PV", "GRID", "STORAGE"]: - self.data[key] = value["currentPower"] + self.data[key] = value.get("currentPower") self.attributes[key] = {"status": value["status"]} if key in ["GRID"]: export = key.lower() in power_to - self.data[key] *= -1 if export else 1 + if self.data[key]: + self.data[key] *= -1 if export else 1 self.attributes[key]["flow"] = "export" if export else "import" if key in ["STORAGE"]: charge = key.lower() in power_to - self.data[key] *= -1 if charge else 1 + if self.data[key]: + self.data[key] *= -1 if charge else 1 self.attributes[key]["flow"] = "charge" if charge else "discharge" self.attributes[key]["soc"] = value["chargeLevel"] From a0eade763200ea402f1c721105839df83c61fda2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 15 Oct 2022 12:15:28 +0300 Subject: [PATCH 169/183] Fix Shelly EM negative power factor (#80348) --- homeassistant/components/shelly/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index bc55aa3e865..fe7ca1a9f83 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -144,7 +144,7 @@ SENSORS: Final = { key="emeter|powerFactor", name="Power Factor", native_unit_of_measurement=PERCENTAGE, - value=lambda value: round(value * 100, 1), + value=lambda value: abs(round(value * 100, 1)), device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), From df75346dca6a239a2bdffc4362c1be6c2309be1d Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Mon, 17 Oct 2022 04:13:11 -0400 Subject: [PATCH 170/183] Fix updating Amcrest binary sensors (#80365) * Fix updating Amcrest binary sensors As detailed in https://bugs.python.org/issue32113, a generator expression cannot be used with asynchronous components, even that the resulting elements of the generator are normal objects. Manually iterate over the event codes and check if the events have happened. Escape early on the first event that is triggered such that this is functionally equivalent to using `any`. * Update homeassistant/components/amcrest/binary_sensor.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/amcrest/binary_sensor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 4d438c7c3bf..e71a5cda538 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -215,10 +215,12 @@ class AmcrestBinarySensor(BinarySensorEntity): raise ValueError(f"Binary sensor {self.name} event codes not set") try: - self._attr_is_on = any( # type: ignore[arg-type] - len(await self._api.async_event_channels_happened(event_code)) > 0 - for event_code in event_codes - ) + for event_code in event_codes: + if await self._api.async_event_channels_happened(event_code): + self._attr_is_on = True + break + else: + self._attr_is_on = False except AmcrestError as error: log_update_error(_LOGGER, "update", self.name, "binary sensor", error) return From c73162e5ea539417a9b2b7761cfc09a1491e3e79 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 18 Oct 2022 09:12:45 +0800 Subject: [PATCH 171/183] Fix stream recorder with orientation transforms (#80370) Find moov instead of using fixed location --- homeassistant/components/stream/fmp4utils.py | 22 +++++++++++++++++--- tests/components/stream/test_hls.py | 2 +- tests/components/stream/test_ll_hls.py | 2 +- tests/components/stream/test_worker.py | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index ed9dd6a9724..35d32d5b0e3 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -4,6 +4,8 @@ from __future__ import annotations from collections.abc import Generator from typing import TYPE_CHECKING +from homeassistant.exceptions import HomeAssistantError + if TYPE_CHECKING: from io import BufferedIOBase @@ -11,7 +13,7 @@ if TYPE_CHECKING: def find_box( mp4_bytes: bytes, target_type: bytes, box_start: int = 0 ) -> Generator[int, None, None]: - """Find location of first box (or sub_box if box_start provided) of given type.""" + """Find location of first box (or sub box if box_start provided) of given type.""" if box_start == 0: index = 0 box_end = len(mp4_bytes) @@ -141,12 +143,26 @@ def get_codec_string(mp4_bytes: bytes) -> str: return ",".join(codecs) +def find_moov(mp4_io: BufferedIOBase) -> int: + """Find location of moov atom in a BufferedIOBase mp4.""" + index = 0 + while 1: + mp4_io.seek(index) + box_header = mp4_io.read(8) + if len(box_header) != 8: + raise HomeAssistantError("moov atom not found") + if box_header[4:8] == b"moov": + return index + index += int.from_bytes(box_header[0:4], byteorder="big") + + def read_init(bytes_io: BufferedIOBase) -> bytes: """Read the init from a mp4 file.""" - bytes_io.seek(24) + moov_loc = find_moov(bytes_io) + bytes_io.seek(moov_loc) moov_len = int.from_bytes(bytes_io.read(4), byteorder="big") bytes_io.seek(0) - return bytes_io.read(24 + moov_len) + return bytes_io.read(moov_loc + moov_len) ZERO32 = b"\x00\x00\x00\x00" diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 204b460b026..e9da793369f 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -29,7 +29,7 @@ from .common import ( from tests.common import async_fire_time_changed STREAM_SOURCE = "some-stream-source" -INIT_BYTES = b"init" +INIT_BYTES = b"\x00\x00\x00\x08moov" FAKE_PAYLOAD = b"fake-payload" SEGMENT_DURATION = 10 TEST_TIMEOUT = 5.0 # Lower than 9s home assistant timeout diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index 5755617f393..baad3043547 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -30,7 +30,7 @@ TEST_PART_DURATION = 0.75 NUM_PART_SEGMENTS = int(-(-SEGMENT_DURATION // TEST_PART_DURATION)) PART_INDEPENDENT_PERIOD = int(1 / TEST_PART_DURATION) or 1 BYTERANGE_LENGTH = 1 -INIT_BYTES = b"init" +INIT_BYTES = b"\x00\x00\x00\x08moov" SEQUENCE_BYTES = bytearray(range(NUM_PART_SEGMENTS * BYTERANGE_LENGTH)) ALT_SEQUENCE_BYTES = bytearray(range(20, 20 + NUM_PART_SEGMENTS * BYTERANGE_LENGTH)) VERY_LARGE_LAST_BYTE_POS = 9007199254740991 diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 00d735df74d..54400af65ab 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -242,7 +242,7 @@ class FakePyAvBuffer: # Forward to appropriate FakeStream packet.stream.mux(packet) # Make new init/part data available to the worker - self.memory_file.write(b"0") + self.memory_file.write(b"\x00\x00\x00\x00moov") def close(self): """Close the buffer.""" From b2e0d7614582be0c2bd7f2a8df72e6b2a402a48c Mon Sep 17 00:00:00 2001 From: Ivan Puddu Date: Tue, 18 Oct 2022 22:53:34 +0200 Subject: [PATCH 172/183] Skip webostv trigger validation before the domain is setup (#80372) * Skip trigger validation before the domain is setup * Included None as return type * Keep function signature intact. Check at the source --- homeassistant/components/webostv/device_trigger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 859accc86e6..ef3e74a7daa 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -46,7 +46,8 @@ async def async_validate_trigger_config( device_id = config[CONF_DEVICE_ID] try: device = async_get_device_entry_by_device_id(hass, device_id) - async_get_client_wrapper_by_device_entry(hass, device) + if DOMAIN in hass.data: + async_get_client_wrapper_by_device_entry(hass, device) except ValueError as err: raise InvalidDeviceAutomationConfig(err) from err From 8c811ef16eaec30075e360821fc2a9e89e7cd621 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Sat, 15 Oct 2022 23:29:15 +0300 Subject: [PATCH 173/183] Fix "Unknown power_off command" for Samsung H6410 (#80386) --- homeassistant/components/samsungtv/bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index f1618bfba14..502c7f2bbb6 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -69,7 +69,7 @@ from .const import ( KEY_PRESS_TIMEOUT = 1.2 -ENCRYPTED_MODEL_USES_POWER_OFF = {"H6400"} +ENCRYPTED_MODEL_USES_POWER_OFF = {"H6400", "H6410"} ENCRYPTED_MODEL_USES_POWER = {"JU6400", "JU641D"} REST_EXCEPTIONS = (HttpApiError, AsyncioTimeoutError, ResponseError) From 7e3abd9db5c7ef5d5ee21bd738b85fea03858fec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Oct 2022 02:14:15 -1000 Subject: [PATCH 174/183] Handle TimeoutError during HKC setup attempts (#80399) closes https://github.com/Jc2k/aiohomekit/issues/188 --- homeassistant/components/homekit_controller/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index dac4afc0b22..54da6e71c8c 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -40,7 +40,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await conn.async_setup() - except (AccessoryNotFoundError, EncryptionError, AccessoryDisconnectedError) as ex: + except ( + asyncio.TimeoutError, + AccessoryNotFoundError, + EncryptionError, + AccessoryDisconnectedError, + ) as ex: del hass.data[KNOWN_DEVICES][conn.unique_id] with contextlib.suppress(asyncio.TimeoutError): await conn.pairing.close() From 0c3d4f2e57c138d32a5e0bc826076f8928879684 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Oct 2022 14:08:26 -1000 Subject: [PATCH 175/183] Bump aiohomekit to 2.0.2 (#80402) --- 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 2f5f8911968..53ec03e5410 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.0.1"], + "requirements": ["aiohomekit==2.0.2"], "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 de635d42499..1318b5adc1f 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.0.1 +aiohomekit==2.0.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca26b35de93..2a0e52ae671 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.0.1 +aiohomekit==2.0.2 # homeassistant.components.emulated_hue # homeassistant.components.http From c76d0f18c8c9cbdc54447d72004c06df05f3a5f9 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 17 Oct 2022 16:54:37 -0400 Subject: [PATCH 176/183] Bump ZHA quirks to 0.0.83 (#80489) --- 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 7067491a12a..5796fc99f3f 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.82", + "zha-quirks==0.0.83", "zigpy-deconz==0.19.0", "zigpy==0.51.3", "zigpy-xbee==0.16.2", diff --git a/requirements_all.txt b/requirements_all.txt index 1318b5adc1f..4a3c0717113 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2592,7 +2592,7 @@ zengge==0.2 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.82 +zha-quirks==0.0.83 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a0e52ae671..4fecba2bbc6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1793,7 +1793,7 @@ youless-api==0.16 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.82 +zha-quirks==0.0.83 # homeassistant.components.zha zigpy-deconz==0.19.0 From 69a698d97a3bd6af7b311b40af4a3b5456cc08e8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 18 Oct 2022 02:40:49 -0600 Subject: [PATCH 177/183] Don't add RainMachine restriction switches if underlying data is missing (#80502) --- homeassistant/components/rainmachine/switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 56ac814e2eb..db560c3c64c 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -34,7 +34,7 @@ from .model import ( RainMachineEntityDescriptionMixinDataKey, RainMachineEntityDescriptionMixinUid, ) -from .util import RUN_STATE_MAP +from .util import RUN_STATE_MAP, key_exists ATTR_AREA = "area" ATTR_CS_ON = "cs_on" @@ -237,6 +237,8 @@ async def async_setup_entry( # Add switches to control restrictions: for description in RESTRICTIONS_SWITCH_DESCRIPTIONS: + if not key_exists(coordinator.data, description.data_key): + continue entities.append(RainMachineRestrictionSwitch(entry, data, description)) async_add_entities(entities) From 605abe58828aa979fd2d3957a9f7bee54fcaec63 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 20 Oct 2022 11:22:30 +0200 Subject: [PATCH 178/183] Add missed write state request for MQTT cover (#80540) Missed write state request for MQTT cover --- homeassistant/components/mqtt/cover.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 6ed12b8adef..11901f15054 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -767,6 +767,7 @@ class MqttCover(MqttEntity, CoverEntity): return position + @callback def tilt_payload_received(self, _payload): """Set the tilt value.""" @@ -784,7 +785,7 @@ class MqttCover(MqttEntity, CoverEntity): ): level = self.find_percentage_in_range(payload) self._tilt_value = level - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) else: _LOGGER.warning( "Payload '%s' is out of range, must be between '%s' and '%s' inclusive", From 8378b768d0d7e9d6b78805fb0ca28c253740becf Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 18 Oct 2022 21:55:50 +0200 Subject: [PATCH 179/183] Save last target temperature in Shelly climate platform (#80561) Save last target temp --- homeassistant/components/shelly/climate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index f98c048d569..e34530d1c8e 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -140,6 +140,7 @@ class BlockSleepingClimate( self.last_state: State | None = None self.last_state_attributes: Mapping[str, Any] self._preset_modes: list[str] = [] + self._last_target_temp = 20.0 if self.block is not None and self.device_block is not None: self._unique_id = f"{self.wrapper.mac}-{self.block.description}" @@ -266,9 +267,15 @@ class BlockSleepingClimate( async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" if hvac_mode == HVACMode.OFF: + if isinstance(self.target_temperature, float): + self._last_target_temp = self.target_temperature await self.set_state_full_path( target_t_enabled=1, target_t=f"{self._attr_min_temp}" ) + if hvac_mode == HVACMode.HEAT: + await self.set_state_full_path( + target_t_enabled=1, target_t=self._last_target_temp + ) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" From fb35896b164be72e4d31ba81bb0804c032b998ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Oct 2022 00:13:41 -0500 Subject: [PATCH 180/183] Bump bluetooth-auto-recovery to 0.3.6 (#80643) * Bump bluetooth-auto-recovery to 0.3.5 changelog: https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/compare/v0.3.4...v0.3.5 * bump again for more rfkill fixes --- 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 783d4ce7df2..d829b46574d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -9,7 +9,7 @@ "bleak==0.18.1", "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", - "bluetooth-auto-recovery==0.3.4", + "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.24.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 183a0205762..3f9168ee19f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ bcrypt==3.1.7 bleak-retry-connector==2.1.3 bleak==0.18.1 bluetooth-adapters==0.6.0 -bluetooth-auto-recovery==0.3.4 +bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4a3c0717113..c869fcfa433 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -435,7 +435,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.4 +bluetooth-auto-recovery==0.3.6 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4fecba2bbc6..0c03bd6ec60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,7 +349,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.4 +bluetooth-auto-recovery==0.3.6 # homeassistant.components.bond bond-async==0.1.22 From 799f7769563469288d6853d1709f9be0d153c134 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 Oct 2022 18:30:00 +0200 Subject: [PATCH 181/183] Pin uamqp==1.6.0 (#80678) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f9168ee19f..2793a383050 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -132,3 +132,6 @@ iso4217!=1.10.20220401 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10 pandas==1.4.3 + +# uamqp 1.6.1, has 1 failing test during built on armv7/armhf +uamqp==1.6.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d0eb830f088..3c2f92cf649 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -142,6 +142,9 @@ iso4217!=1.10.20220401 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10 pandas==1.4.3 + +# uamqp 1.6.1, has 1 failing test during built on armv7/armhf +uamqp==1.6.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 44d35fc8e4a779f0e6902d593fc1fa61d3b65d91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Oct 2022 13:35:38 -0500 Subject: [PATCH 182/183] Fix bluetooth calls from automations in esphome (#80683) --- homeassistant/components/bluetooth/models.py | 2 ++ homeassistant/components/esphome/bluetooth/client.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 9e93ea4d142..4ea44bcbe9b 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -279,6 +279,7 @@ class HaBleakClientWrapper(BleakClient): async def connect(self, **kwargs: Any) -> bool: """Connect to the specified GATT server.""" if not self._backend: + assert MANAGER is not None wrapped_backend = ( self._async_get_backend() or await self._async_get_fallback_backend() ) @@ -287,6 +288,7 @@ class HaBleakClientWrapper(BleakClient): or wrapped_backend.device, disconnected_callback=self.__disconnected_callback, timeout=self.__timeout, + hass=MANAGER.hass, ) return await super().connect(**kwargs) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 14924756074..eda75436502 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -15,7 +15,7 @@ from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakError -from homeassistant.core import CALLBACK_TYPE, async_get_hass +from homeassistant.core import CALLBACK_TYPE from ..domain_data import DomainData from .characteristic import BleakGATTCharacteristicESPHome @@ -83,7 +83,7 @@ class ESPHomeClient(BaseBleakClient): self._address_as_int = mac_to_int(self._ble_device.address) assert self._ble_device.details is not None self._source = self._ble_device.details["source"] - self.domain_data = DomainData.get(async_get_hass()) + self.domain_data = DomainData.get(kwargs["hass"]) config_entry = self.domain_data.get_by_unique_id(self._source) self.entry_data = self.domain_data.get_entry_data(config_entry) self._client = self.entry_data.client From c2dff8fb7e132f3cd692b23a86f2e4c58eab1cca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 20 Oct 2022 14:49:21 -0400 Subject: [PATCH 183/183] Bumped version to 2022.10.5 --- 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 b60ab0d17e9..da1e691903f 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 = 10 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __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 9a6c1458742..5ea26ef1976 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.4" +version = "2022.10.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"