diff --git a/CODEOWNERS b/CODEOWNERS index 4d4c7d3d900..3e2959cc043 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,7 +77,7 @@ homeassistant/components/counter/* @fabaff homeassistant/components/cover/* @home-assistant/core homeassistant/components/cpuspeed/* @fabaff homeassistant/components/cups/* @fabaff -homeassistant/components/daikin/* @fredrike @rofrantz +homeassistant/components/daikin/* @fredrike homeassistant/components/darksky/* @fabaff homeassistant/components/deconz/* @kane610 homeassistant/components/delijn/* @bollewolle diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index f3f2397d214..d50ba727467 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -347,12 +347,17 @@ class AmbientStation: async def _attempt_connect(self): """Attempt to connect to the socket (retrying later on fail).""" - try: + + async def connect(timestamp=None): + """Connect.""" await self.client.websocket.connect() + + try: + await connect() except WebsocketError as err: _LOGGER.error("Error with the websocket connection: %s", err) self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) - async_call_later(self._hass, self._ws_reconnect_delay, self.ws_connect) + async_call_later(self._hass, self._ws_reconnect_delay, connect) async def ws_connect(self): """Register handlers and connect to the websocket.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index c532a2063a7..69b5796e8ba 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -482,7 +482,7 @@ async def google_assistant_list(hass, connection, msg): { "entity_id": entity.entity_id, "traits": [trait.name for trait in entity.traits()], - "might_2fa": entity.might_2fa(), + "might_2fa": entity.might_2fa_traits(), } ) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 209bf71e594..f6384cfd4b8 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -17,6 +17,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from . import config_flow # noqa: F401 +from .const import TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -91,7 +92,7 @@ async def daikin_api_setup(hass, host): session = hass.helpers.aiohttp_client.async_get_clientsession() try: - with timeout(10): + with timeout(TIMEOUT): device = Appliance(host, session) await device.init() except asyncio.TimeoutError: diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index d46ea26d487..cd45fbdd74b 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -84,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Daikin climate based on config_entry.""" daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id) - async_add_entities([DaikinClimate(daikin_api)]) + async_add_entities([DaikinClimate(daikin_api)], update_before_add=True) class DaikinClimate(ClimateDevice): diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index bd90a87db86..35f21ef3e0d 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST -from .const import KEY_IP, KEY_MAC +from .const import KEY_IP, KEY_MAC, TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ class FlowHandler(config_entries.ConfigFlow): device = Appliance( host, self.hass.helpers.aiohttp_client.async_get_clientsession() ) - with timeout(10): + with timeout(TIMEOUT): await device.init() except asyncio.TimeoutError: return self.async_abort(reason="device_timeout") diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index ef24a51be89..15ae5321bf3 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -25,3 +25,5 @@ SENSOR_TYPES = { KEY_MAC = "mac" KEY_IP = "ip" + +TIMEOUT = 60 diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index a752642335f..c501fa7c120 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,8 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==1.6.2"], - "dependencies": [], - "codeowners": ["@fredrike", "@rofrantz"], + "requirements": ["pydaikin==1.6.3"], + "codeowners": ["@fredrike"], "quality_scale": "platinum" } diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 6ba301c01e8..bbdb8a82183 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -372,14 +372,19 @@ class GoogleEntity: @callback def might_2fa(self) -> bool: """Return if the entity might encounter 2FA.""" + if not self.config.should_2fa(self.state): + return False + + return self.might_2fa_traits() + + @callback + def might_2fa_traits(self) -> bool: + """Return if the entity might encounter 2FA based on just traits.""" state = self.state domain = state.domain features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) device_class = state.attributes.get(ATTR_DEVICE_CLASS) - if not self.config.should_2fa(state): - return False - return any( trait.might_2fa(domain, features, device_class) for trait in self.traits() ) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index b7239c8bf49..32474881a87 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -6,8 +6,10 @@ from pyipp import ( IPP, IPPConnectionError, IPPConnectionUpgradeRequired, + IPPError, IPPParseError, IPPResponseError, + IPPVersionNotSupportedError, ) import voluptuous as vol @@ -70,10 +72,16 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): except IPPConnectionUpgradeRequired: return self._show_setup_form({"base": "connection_upgrade"}) except (IPPConnectionError, IPPResponseError): + _LOGGER.debug("IPP Connection/Response Error", exc_info=True) return self._show_setup_form({"base": "connection_error"}) except IPPParseError: - _LOGGER.exception("IPP Parse Error") + _LOGGER.debug("IPP Parse Error", exc_info=True) return self.async_abort(reason="parse_error") + except IPPVersionNotSupportedError: + return self.async_abort(reason="ipp_version_error") + except IPPError: + _LOGGER.debug("IPP Error", exc_info=True) + return self.async_abort(reason="ipp_error") user_input[CONF_UUID] = info[CONF_UUID] @@ -111,10 +119,16 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): except IPPConnectionUpgradeRequired: return self.async_abort(reason="connection_upgrade") except (IPPConnectionError, IPPResponseError): + _LOGGER.debug("IPP Connection/Response Error", exc_info=True) return self.async_abort(reason="connection_error") except IPPParseError: - _LOGGER.exception("IPP Parse Error") + _LOGGER.debug("IPP Parse Error", exc_info=True) return self.async_abort(reason="parse_error") + except IPPVersionNotSupportedError: + return self.async_abort(reason="ipp_version_error") + except IPPError: + _LOGGER.debug("IPP Error", exc_info=True) + return self.async_abort(reason="ipp_error") if info[CONF_UUID] is not None: self.discovery_info[CONF_UUID] = info[CONF_UUID] diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 0cb7c108b63..216ee519a3a 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -2,7 +2,7 @@ "domain": "ipp", "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", - "requirements": ["pyipp==0.9.2"], + "requirements": ["pyipp==0.10.1"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index a80a7f2e0ba..c77d1eec161 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -27,6 +27,8 @@ "already_configured": "This printer is already configured.", "connection_error": "Failed to connect to printer.", "connection_upgrade": "Failed to connect to printer due to connection upgrade being required.", + "ipp_error": "Encountered IPP error.", + "ipp_version_error": "IPP version not supported by printer.", "parse_error": "Failed to parse response from printer." } } diff --git a/homeassistant/const.py b/homeassistant/const.py index da6dd4bb24e..a1998252008 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "4" +PATCH_VERSION = "5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) diff --git a/requirements_all.txt b/requirements_all.txt index 36c54eab31b..43a036b19f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ pycsspeechtts==1.0.3 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==1.6.2 +pydaikin==1.6.3 # homeassistant.components.danfoss_air pydanfossair==0.1.0 @@ -1336,7 +1336,7 @@ pyintesishome==1.7.3 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.9.2 +pyipp==0.10.1 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 531b2550dea..f5318ffc955 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -479,7 +479,7 @@ pychromecast==4.2.0 pycoolmasternet==0.0.4 # homeassistant.components.daikin -pydaikin==1.6.2 +pydaikin==1.6.3 # homeassistant.components.deconz pydeconz==70 @@ -519,7 +519,7 @@ pyicloud==0.9.6.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.9.2 +pyipp==0.10.1 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/rootfs/init b/rootfs/init new file mode 100755 index 00000000000..7bea7ed88a9 --- /dev/null +++ b/rootfs/init @@ -0,0 +1,23 @@ +#!/bin/execlineb -S0 + +## +## load default PATH (the same that Docker includes if not provided) if it doesn't exist, +## then go ahead with stage1. +## this was motivated due to this issue: +## - https://github.com/just-containers/s6-overlay/issues/108 +## + +/bin/importas -D /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PATH PATH +export PATH ${PATH} + +## +## Skip further init if the user has a given CMD. +## This is to prevent Home Assistant from starting twice if the user +## decided to override/start via the CMD. +## + +ifelse { s6-test $# -ne 0 } +{ + $@ +} +/etc/s6/init/init-stage1 $@ \ No newline at end of file diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 8bfa6185e9b..d6bca023064 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -687,20 +687,30 @@ async def test_list_google_entities(hass, hass_ws_client, setup_api, mock_cloud_ entity = GoogleEntity( hass, MockConfig(should_expose=lambda *_: False), State("light.kitchen", "on") ) + entity2 = GoogleEntity( + hass, + MockConfig(should_expose=lambda *_: True, should_2fa=lambda *_: False), + State("cover.garage", "open", {"device_class": "garage"}), + ) with patch( "homeassistant.components.google_assistant.helpers.async_get_entities", - return_value=[entity], + return_value=[entity, entity2], ): await client.send_json({"id": 5, "type": "cloud/google_assistant/entities"}) response = await client.receive_json() assert response["success"] - assert len(response["result"]) == 1 + assert len(response["result"]) == 2 assert response["result"][0] == { "entity_id": "light.kitchen", "might_2fa": False, "traits": ["action.devices.traits.OnOff"], } + assert response["result"][1] == { + "entity_id": "cover.garage", + "might_2fa": True, + "traits": ["action.devices.traits.OpenClose"], + } async def test_update_google_entity(hass, hass_ws_client, setup_api, mock_cloud_login): diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 802b7968ee6..79684bdeb44 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -33,6 +33,7 @@ class MockConfig(helpers.AbstractConfig): """Initialize config.""" super().__init__(hass) self._should_expose = should_expose + self._should_2fa = should_2fa self._secure_devices_pin = secure_devices_pin self._entity_config = entity_config or {} self._local_sdk_webhook_id = local_sdk_webhook_id @@ -73,6 +74,10 @@ class MockConfig(helpers.AbstractConfig): """Expose it all.""" return self._should_expose is None or self._should_expose(state) + def should_2fa(self, state): + """Expose it all.""" + return self._should_2fa is None or self._should_2fa(state) + BASIC_CONFIG = MockConfig() diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index d0ed9a9d33c..a2b8f2e9ea7 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -845,10 +845,8 @@ async def test_lock_unlock_unlock(hass): assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP # Test with 2FA override - with patch( - "homeassistant.components.google_assistant.helpers" - ".AbstractConfig.should_2fa", - return_value=False, + with patch.object( + BASIC_CONFIG, "should_2fa", return_value=False, ): await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {"lock": False}, {}) assert len(calls) == 2 diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 7e16a9fc6e0..5229881fbf4 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -1,6 +1,6 @@ """Tests for the IPP config flow.""" import aiohttp -from pyipp import IPPConnectionUpgradeRequired +from pyipp import IPPConnectionUpgradeRequired, IPPError from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF @@ -172,6 +172,74 @@ async def test_zeroconf_parse_error( assert result["reason"] == "parse_error" +async def test_user_ipp_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort the user flow on IPP error.""" + aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ipp_error" + + +async def test_zeroconf_ipp_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on IPP error.""" + aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError) + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ipp_error" + + +async def test_user_ipp_version_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort user flow on IPP version not supported error.""" + aioclient_mock.post( + "http://192.168.1.31:631/ipp/print", + content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"), + headers={"Content-Type": "application/ipp"}, + ) + + user_input = {**MOCK_USER_INPUT} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ipp_version_error" + + +async def test_zeroconf_ipp_version_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on IPP version not supported error.""" + aioclient_mock.post( + "http://192.168.1.31:631/ipp/print", + content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"), + headers={"Content-Type": "application/ipp"}, + ) + + discovery_info = {**MOCK_ZEROCONF_IPP_SERVICE_INFO} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ipp_version_error" + + async def test_user_device_exists_abort( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/fixtures/ipp/get-printer-attributes-error-0x0503.bin b/tests/fixtures/ipp/get-printer-attributes-error-0x0503.bin new file mode 100644 index 00000000000..c92134b9e3b Binary files /dev/null and b/tests/fixtures/ipp/get-printer-attributes-error-0x0503.bin differ