From 48d9e9a83ce9f4d75386fc9251c358bf98b5508b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 2 Mar 2022 16:41:03 -0600 Subject: [PATCH 1/8] Bump soco to 0.26.4 (#67498) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 4bb8623acb2..6f482e92dc9 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.26.3"], + "requirements": ["soco==0.26.4"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 9714786a873..bf18ddba431 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2235,7 +2235,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.26.3 +soco==0.26.4 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f539520713..d32cd1609b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1371,7 +1371,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.26.3 +soco==0.26.4 # homeassistant.components.solaredge solaredge==0.0.2 From ee0bdaa2dea1031bd120a762e7e97cc97155ecd1 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Mar 2022 00:41:50 +0100 Subject: [PATCH 2/8] Check if UPnP is enabled on Fritz device (#67512) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/fritz/__init__.py | 6 ++++++ homeassistant/components/fritz/common.py | 10 ++++++++++ homeassistant/components/fritz/config_flow.py | 7 +++++++ homeassistant/components/fritz/const.py | 1 + homeassistant/components/fritz/strings.json | 1 + homeassistant/components/fritz/translations/en.json | 5 +++-- 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index a0e0413366b..0b334ff616a 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -33,6 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except FRITZ_EXCEPTIONS as ex: raise ConfigEntryNotReady from ex + if ( + "X_AVM-DE_UPnP1" in avm_wrapper.connection.services + and not (await avm_wrapper.async_get_upnp_configuration())["NewEnable"] + ): + raise ConfigEntryAuthFailed("Missing UPnP configuration") + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = avm_wrapper diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index b2a429bfa3c..2fc28433e56 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -630,6 +630,11 @@ class AvmWrapper(FritzBoxTools): ) return {} + async def async_get_upnp_configuration(self) -> dict[str, Any]: + """Call X_AVM-DE_UPnP service.""" + + return await self.hass.async_add_executor_job(self.get_upnp_configuration) + async def async_get_wan_link_properties(self) -> dict[str, Any]: """Call WANCommonInterfaceConfig service.""" @@ -698,6 +703,11 @@ class AvmWrapper(FritzBoxTools): partial(self.set_allow_wan_access, ip_address, turn_on) ) + def get_upnp_configuration(self) -> dict[str, Any]: + """Call X_AVM-DE_UPnP service.""" + + return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo") + def get_ontel_num_deflections(self) -> dict[str, Any]: """Call GetNumberOfDeflections action from X_AVM-DE_OnTel service.""" diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 0844d725522..046f00ba3a9 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -29,6 +29,7 @@ from .const import ( ERROR_AUTH_INVALID, ERROR_CANNOT_CONNECT, ERROR_UNKNOWN, + ERROR_UPNP_NOT_CONFIGURED, ) _LOGGER = logging.getLogger(__name__) @@ -79,6 +80,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") return ERROR_UNKNOWN + if ( + "X_AVM-DE_UPnP1" in self.avm_wrapper.connection.services + and not (await self.avm_wrapper.async_get_upnp_configuration())["NewEnable"] + ): + return ERROR_UPNP_NOT_CONFIGURED + return None async def async_check_configured_entry(self) -> ConfigEntry | None: diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index f33cf463996..f739ccf6858 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -46,6 +46,7 @@ DEFAULT_USERNAME = "" ERROR_AUTH_INVALID = "invalid_auth" ERROR_CANNOT_CONNECT = "cannot_connect" +ERROR_UPNP_NOT_CONFIGURED = "upnp_not_configured" ERROR_UNKNOWN = "unknown_error" FRITZ_SERVICES = "fritz_services" diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 450566f101b..a65b2900f66 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -36,6 +36,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "upnp_not_configured": "Missing UPnP settings on device.", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index 0a58ee686f3..c6fa4a16036 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -9,7 +9,8 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "upnp_not_configured": "Missing UPnP settings on device." }, "flow_title": "{name}", "step": { @@ -51,4 +52,4 @@ } } } -} \ No newline at end of file +} From 63f8e9ee08747301df60ca2a37c177fd88b83470 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 3 Mar 2022 21:40:15 +0100 Subject: [PATCH 3/8] Fix MQTT config flow with advanced parameters (#67556) * Fix MQTT config flow with advanced parameters * Add test --- homeassistant/components/mqtt/__init__.py | 106 +++++++++++-------- homeassistant/components/mqtt/config_flow.py | 29 ++--- homeassistant/components/mqtt/const.py | 7 ++ tests/components/mqtt/test_config_flow.py | 97 ++++++++++++++++- 4 files changed, 177 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 107bc4660c2..c6229c2d475 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -75,11 +75,16 @@ from .const import ( ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + CONF_TLS_INSECURE, + CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, DATA_MQTT_CONFIG, @@ -94,6 +99,7 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + PROTOCOL_31, PROTOCOL_311, ) from .discovery import LAST_DISCOVERY @@ -118,13 +124,6 @@ SERVICE_DUMP = "dump" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_KEEPALIVE = "keepalive" -CONF_CERTIFICATE = "certificate" -CONF_CLIENT_KEY = "client_key" -CONF_CLIENT_CERT = "client_cert" -CONF_TLS_INSECURE = "tls_insecure" -CONF_TLS_VERSION = "tls_version" - -PROTOCOL_31 = "3.1" DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 @@ -757,6 +756,58 @@ class Subscription: encoding: str | None = attr.ib(default="utf-8") +class MqttClientSetup: + """Helper class to setup the paho mqtt client from config.""" + + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + + def __init__(self, config: ConfigType) -> None: + """Initialize the MQTT client setup helper.""" + + if config[CONF_PROTOCOL] == PROTOCOL_31: + proto = self.mqtt.MQTTv31 + else: + proto = self.mqtt.MQTTv311 + + if (client_id := config.get(CONF_CLIENT_ID)) is None: + # PAHO MQTT relies on the MQTT server to generate random client IDs. + # However, that feature is not mandatory so we generate our own. + client_id = self.mqtt.base62(uuid.uuid4().int, padding=22) + self._client = self.mqtt.Client(client_id, protocol=proto) + + # Enable logging + self._client.enable_logger() + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + if username is not None: + self._client.username_pw_set(username, password) + + if (certificate := config.get(CONF_CERTIFICATE)) == "auto": + certificate = certifi.where() + + client_key = config.get(CONF_CLIENT_KEY) + client_cert = config.get(CONF_CLIENT_CERT) + tls_insecure = config.get(CONF_TLS_INSECURE) + if certificate is not None: + self._client.tls_set( + certificate, + certfile=client_cert, + keyfile=client_key, + tls_version=ssl.PROTOCOL_TLS, + ) + + if tls_insecure is not None: + self._client.tls_insecure_set(tls_insecure) + + @property + def client(self) -> mqtt.Client: + """Return the paho MQTT client.""" + return self._client + + class MQTT: """Home Assistant MQTT client.""" @@ -821,46 +872,7 @@ class MQTT: def init_client(self): """Initialize paho client.""" - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - - if self.conf[CONF_PROTOCOL] == PROTOCOL_31: - proto: int = mqtt.MQTTv31 - else: - proto = mqtt.MQTTv311 - - if (client_id := self.conf.get(CONF_CLIENT_ID)) is None: - # PAHO MQTT relies on the MQTT server to generate random client IDs. - # However, that feature is not mandatory so we generate our own. - client_id = mqtt.base62(uuid.uuid4().int, padding=22) - self._mqttc = mqtt.Client(client_id, protocol=proto) - - # Enable logging - self._mqttc.enable_logger() - - username = self.conf.get(CONF_USERNAME) - password = self.conf.get(CONF_PASSWORD) - if username is not None: - self._mqttc.username_pw_set(username, password) - - if (certificate := self.conf.get(CONF_CERTIFICATE)) == "auto": - certificate = certifi.where() - - client_key = self.conf.get(CONF_CLIENT_KEY) - client_cert = self.conf.get(CONF_CLIENT_CERT) - tls_insecure = self.conf.get(CONF_TLS_INSECURE) - if certificate is not None: - self._mqttc.tls_set( - certificate, - certfile=client_cert, - keyfile=client_key, - tls_version=ssl.PROTOCOL_TLS, - ) - - if tls_insecure is not None: - self._mqttc.tls_insecure_set(tls_insecure) - + self._mqttc = MqttClientSetup(self.conf).client self._mqttc.on_connect = self._mqtt_on_connect self._mqttc.on_disconnect = self._mqtt_on_disconnect self._mqttc.on_message = self._mqtt_on_message diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 3f93e50829a..99e7e9718d0 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResult +from . import MqttClientSetup from .const import ( ATTR_PAYLOAD, ATTR_QOS, @@ -62,6 +63,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, + self.hass, user_input[CONF_BROKER], user_input[CONF_PORT], user_input.get(CONF_USERNAME), @@ -102,6 +104,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = self._hassio_discovery can_connect = await self.hass.async_add_executor_job( try_connection, + self.hass, data[CONF_HOST], data[CONF_PORT], data.get(CONF_USERNAME), @@ -152,6 +155,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, + self.hass, user_input[CONF_BROKER], user_input[CONF_PORT], user_input.get(CONF_USERNAME), @@ -313,25 +317,24 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) -def try_connection(broker, port, username, password, protocol="3.1"): +def try_connection(hass, broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - if protocol == "3.1": - proto = mqtt.MQTTv31 - else: - proto = mqtt.MQTTv311 - - client = mqtt.Client(protocol=proto) - if username and password: - client.username_pw_set(username, password) + # Get the config from configuration.yaml + yaml_config = hass.data.get(DATA_MQTT_CONFIG, {}) + entry_config = { + CONF_BROKER: broker, + CONF_PORT: port, + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_PROTOCOL: protocol, + } + client = MqttClientSetup({**yaml_config, **entry_config}).client result = queue.Queue(maxsize=1) def on_connect(client_, userdata, flags, result_code): """Handle connection result.""" - result.put(result_code == mqtt.CONNACK_ACCEPTED) + result.put(result_code == MqttClientSetup.mqtt.CONNACK_ACCEPTED) client.on_connect = on_connect diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index f04348ee002..69865733763 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -22,6 +22,12 @@ CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_TOPIC = "topic" CONF_WILL_MESSAGE = "will_message" +CONF_CERTIFICATE = "certificate" +CONF_CLIENT_KEY = "client_key" +CONF_CLIENT_CERT = "client_cert" +CONF_TLS_INSECURE = "tls_insecure" +CONF_TLS_VERSION = "tls_version" + DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" @@ -56,4 +62,5 @@ MQTT_DISCONNECTED = "mqtt_disconnected" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" +PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index d9aab02e821..88c6137bf94 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -3,8 +3,9 @@ from unittest.mock import patch import pytest import voluptuous as vol +import yaml -from homeassistant import config_entries, data_entry_flow +from homeassistant import config as hass_config, config_entries, data_entry_flow from homeassistant.components import mqtt from homeassistant.components.hassio import HassioServiceInfo from homeassistant.core import HomeAssistant @@ -151,7 +152,7 @@ async def test_manual_config_set( "discovery": True, } # Check we tried the connection, with precedence for config entry settings - mock_try_connection.assert_called_once_with("127.0.0.1", 1883, None, None) + mock_try_connection.assert_called_once_with(hass, "127.0.0.1", 1883, None, None) # Check config entry got setup assert len(mock_finish_setup.mock_calls) == 1 @@ -642,3 +643,95 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection): mqtt.CONF_BROKER: "test-broker", mqtt.CONF_PORT: 1234, } + + +async def test_try_connection_with_advanced_parameters( + hass, mock_try_connection_success, tmp_path +): + """Test config flow with advanced parameters from config.""" + # Mock certificate files + certfile = tmp_path / "cert.pem" + certfile.write_text("## mock certificate file ##") + keyfile = tmp_path / "key.pem" + keyfile.write_text("## mock key file ##") + config = { + "certificate": "auto", + "tls_insecure": True, + "client_cert": certfile, + "client_key": keyfile, + } + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({mqtt.DOMAIN: config}) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry.add_to_hass(hass) + config_entry.data = { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "pass", + mqtt.CONF_DISCOVERY: True, + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/online", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 1, + mqtt.ATTR_RETAIN: True, + }, + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/offline", + mqtt.ATTR_PAYLOAD: "offline", + mqtt.ATTR_QOS: 2, + mqtt.ATTR_RETAIN: False, + }, + } + + # Test default/suggested values from config + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "broker" + defaults = { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + } + suggested = { + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "pass", + } + for k, v in defaults.items(): + assert get_default(result["data_schema"].schema, k) == v + for k, v in suggested.items(): + assert get_suggested(result["data_schema"].schema, k) == v + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "another-broker", + mqtt.CONF_PORT: 2345, + mqtt.CONF_USERNAME: "us3r", + mqtt.CONF_PASSWORD: "p4ss", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + + # check if the username and password was set from config flow and not from configuration.yaml + assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == ( + "us3r", + "p4ss", + ) + + # check if tls_insecure_set is called + assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,) + + # check if the certificate settings were set from configuration.yaml + assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[ + "certfile" + ] == str(certfile) + assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[ + "keyfile" + ] == str(keyfile) From eff7a12557f53b95f23b3cd88eadc10146d0b6ce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Mar 2022 15:03:03 -0800 Subject: [PATCH 4/8] Highlight in logs it is a custom component when setup fails (#67559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/setup.py | 18 +++++++++++++----- tests/test_setup.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 36292989dce..2599b4b3c85 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -149,10 +149,17 @@ async def _async_setup_component( This method is a coroutine. """ + integration: loader.Integration | None = None - def log_error(msg: str, link: str | None = None) -> None: + def log_error(msg: str) -> None: """Log helper.""" - _LOGGER.error("Setup failed for %s: %s", domain, msg) + if integration is None: + custom = "" + link = None + else: + custom = "" if integration.is_built_in else "custom integration " + link = integration.documentation + _LOGGER.error("Setup failed for %s%s: %s", custom, domain, msg) async_notify_setup_error(hass, domain, link) try: @@ -174,7 +181,7 @@ async def _async_setup_component( try: await async_process_deps_reqs(hass, config, integration) except HomeAssistantError as err: - log_error(str(err), integration.documentation) + log_error(str(err)) return False # Some integrations fail on import because they call functions incorrectly. @@ -182,7 +189,7 @@ async def _async_setup_component( try: component = integration.get_component() except ImportError as err: - log_error(f"Unable to import component: {err}", integration.documentation) + log_error(f"Unable to import component: {err}") return False processed_config = await conf_util.async_process_component_config( @@ -190,7 +197,7 @@ async def _async_setup_component( ) if processed_config is None: - log_error("Invalid config.", integration.documentation) + log_error("Invalid config.") return False start = timer() @@ -287,6 +294,7 @@ async def async_prepare_setup_platform( def log_error(msg: str) -> None: """Log helper.""" + _LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg) async_notify_setup_error(hass, platform_path) diff --git a/tests/test_setup.py b/tests/test_setup.py index f71ba01410b..04924344c2b 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries, setup from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, @@ -621,6 +622,22 @@ async def test_integration_disabled(hass, caplog): assert disabled_reason in caplog.text +async def test_integration_logs_is_custom(hass, caplog): + """Test we highlight it's a custom component when errors happen.""" + mock_integration( + hass, + MockModule("test_component1"), + built_in=False, + ) + with patch( + "homeassistant.setup.async_process_deps_reqs", + side_effect=HomeAssistantError("Boom"), + ): + result = await setup.async_setup_component(hass, "test_component1", {}) + assert not result + assert "Setup failed for custom integration test_component1: Boom" in caplog.text + + async def test_async_get_loaded_integrations(hass): """Test we can enumerate loaded integations.""" hass.config.components.add("notbase") From d36164350011290b28446bc30c4d8021c49997a8 Mon Sep 17 00:00:00 2001 From: Emory Penney Date: Thu, 3 Mar 2022 15:22:36 -0800 Subject: [PATCH 5/8] Bump pyobihai (#67571) --- homeassistant/components/obihai/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index f908ad16179..96f803cebdd 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -2,7 +2,7 @@ "domain": "obihai", "name": "Obihai", "documentation": "https://www.home-assistant.io/integrations/obihai", - "requirements": ["pyobihai==1.3.1"], + "requirements": ["pyobihai==1.3.2"], "codeowners": ["@dshokouhi"], "iot_class": "local_polling", "loggers": ["pyobihai"] diff --git a/requirements_all.txt b/requirements_all.txt index bf18ddba431..84f1dbb2004 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1727,7 +1727,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.3.1 +pyobihai==1.3.2 # homeassistant.components.octoprint pyoctoprintapi==0.1.7 From b5b945ab4d97d6f3441a39943d1ef0c48e20f922 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Thu, 3 Mar 2022 23:05:13 +0000 Subject: [PATCH 6/8] Fix data type for growatt lastdataupdate (#67511) (#67582) Co-authored-by: Paulus Schoutsen --- homeassistant/components/growatt_server/sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 71c1c69e08a..67095492de7 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -221,12 +221,9 @@ class GrowattData: # Create datetime from the latest entry date_now = dt.now().date() last_updated_time = dt.parse_time(str(sorted_keys[-1])) - combined_timestamp = datetime.datetime.combine( + mix_detail["lastdataupdate"] = datetime.datetime.combine( date_now, last_updated_time ) - # Convert datetime to UTC - combined_timestamp_utc = dt.as_utc(combined_timestamp) - mix_detail["lastdataupdate"] = combined_timestamp_utc.isoformat() # Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined # imported from grid value that is the combination of charging AND load consumption From 73765a1f2914ab2401dd86e2763e0d7c9df40996 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Mar 2022 13:03:46 -1000 Subject: [PATCH 7/8] Add guards for HomeKit version/names that break apple watches (#67585) --- .../components/homekit/accessories.py | 8 +++-- homeassistant/components/homekit/util.py | 31 +++++++++++++++---- tests/components/homekit/test_accessories.py | 17 +++++++--- tests/components/homekit/test_type_sensors.py | 2 +- tests/components/homekit/test_util.py | 15 +++++++-- 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index d348b4c1f42..4129c3225b7 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -274,7 +274,7 @@ class HomeAccessory(Accessory): if self.config.get(ATTR_SW_VERSION) is not None: sw_version = format_version(self.config[ATTR_SW_VERSION]) if sw_version is None: - sw_version = __version__ + sw_version = format_version(__version__) hw_version = None if self.config.get(ATTR_HW_VERSION) is not None: hw_version = format_version(self.config[ATTR_HW_VERSION]) @@ -289,7 +289,9 @@ class HomeAccessory(Accessory): serv_info = self.get_service(SERV_ACCESSORY_INFO) char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION) serv_info.add_characteristic(char) - serv_info.configure_char(CHAR_HARDWARE_REVISION, value=hw_version) + serv_info.configure_char( + CHAR_HARDWARE_REVISION, value=hw_version[:MAX_VERSION_LENGTH] + ) self.iid_manager.assign(char) char.broker = self @@ -532,7 +534,7 @@ class HomeBridge(Bridge): """Initialize a Bridge object.""" super().__init__(driver, name) self.set_info_service( - firmware_revision=__version__, + firmware_revision=format_version(__version__), manufacturer=MANUFACTURER, model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER, diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 8c64b9b0443..7fa4ffa8bf6 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -100,6 +100,7 @@ _LOGGER = logging.getLogger(__name__) NUMBERS_ONLY_RE = re.compile(r"[^\d.]+") VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?") +MAX_VERSION_PART = 2**32 - 1 MAX_PORT = 65535 @@ -363,7 +364,15 @@ def convert_to_float(state): return None -def cleanup_name_for_homekit(name: str | None) -> str | None: +def coerce_int(state: str) -> int: + """Return int.""" + try: + return int(state) + except (ValueError, TypeError): + return 0 + + +def cleanup_name_for_homekit(name: str | None) -> str: """Ensure the name of the device will not crash homekit.""" # # This is not a security measure. @@ -371,7 +380,7 @@ def cleanup_name_for_homekit(name: str | None) -> str | None: # UNICODE_EMOJI is also not allowed but that # likely isn't a problem if name is None: - return None + return "None" # None crashes apple watches return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH] @@ -420,13 +429,23 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): ) +def _format_version_part(version_part: str) -> str: + return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part)))) + + def format_version(version): """Extract the version string in a format homekit can consume.""" - split_ver = str(version).replace("-", ".") + split_ver = str(version).replace("-", ".").replace(" ", ".") num_only = NUMBERS_ONLY_RE.sub("", split_ver) - if match := VERSION_RE.search(num_only): - return match.group(0) - return None + if (match := VERSION_RE.search(num_only)) is None: + return None + value = ".".join(map(_format_version_part, match.group(0).split("."))) + return None if _is_zero_but_true(value) else value + + +def _is_zero_but_true(value): + """Zero but true values can crash apple watches.""" + return convert_to_float(value) == 0 def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 103ee9ea2da..704bb368d64 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -42,7 +42,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, - __version__, __version__ as hass_version, ) from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS @@ -166,7 +165,9 @@ async def test_home_accessory(hass, hk_driver): serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" ) - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) hass.states.async_set(entity_id, "on") await hass.async_block_till_done() @@ -216,7 +217,9 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert isinstance(acc.to_HAP(), dict) @@ -244,7 +247,9 @@ async def test_accessory_with_hardware_revision(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3" assert isinstance(acc.to_HAP(), dict) @@ -687,7 +692,9 @@ def test_home_bridge(hk_driver): serv = bridge.services[0] # SERV_ACCESSORY_INFO assert serv.display_name == SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == __version__ + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == BRIDGE_SERIAL_NUMBER diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index d864a90fe61..9b6d1c9cee2 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -399,4 +399,4 @@ async def test_empty_name(hass, hk_driver): assert acc.category == 10 # Sensor assert acc.char_humidity.value == 20 - assert acc.display_name is None + assert acc.display_name == "None" diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0432fb27426..3dd30af2056 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -30,6 +30,7 @@ from homeassistant.components.homekit.util import ( async_port_is_available, async_show_setup_message, cleanup_name_for_homekit, + coerce_int, convert_to_float, density_to_air_quality, format_version, @@ -349,13 +350,23 @@ async def test_format_version(): assert format_version("undefined-undefined-1.6.8") == "1.6.8" assert format_version("56.0-76060") == "56.0.76060" assert format_version(3.6) == "3.6" - assert format_version("AK001-ZJ100") == "001.100" + assert format_version("AK001-ZJ100") == "1.100" assert format_version("HF-LPB100-") == "100" - assert format_version("AK001-ZJ2149") == "001.2149" + assert format_version("AK001-ZJ2149") == "1.2149" + assert format_version("13216407885") == "4294967295" # max value + assert format_version("000132 16407885") == "132.16407885" assert format_version("0.1") == "0.1" + assert format_version("0") is None assert format_version("unknown") is None +async def test_coerce_int(): + """Test coerce_int method.""" + assert coerce_int("1") == 1 + assert coerce_int("") == 0 + assert coerce_int(0) == 0 + + async def test_accessory_friendly_name(): """Test we provide a helpful friendly name.""" From ba40d62081eb81f5101a6c74ac04a4859f8e54cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Mar 2022 15:53:54 -0800 Subject: [PATCH 8/8] Bumped version to 2022.3.1 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c0ff111fc89..9c546af49f8 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 = 3 -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/setup.cfg b/setup.cfg index d72829b574e..1d731bfdb9c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0 +version = 2022.3.1 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0