From 5b5755f5e2f0bd7154b34d0ae3ce51d529fbc954 Mon Sep 17 00:00:00 2001 From: Christopher Gozdziewski Date: Thu, 4 Feb 2021 01:32:43 -0600 Subject: [PATCH 01/14] Convert ozw climate values to correct units (#45369) * Convert ozw climate values to correct units * Remove debugger logging * Fix black code formatting * Remove extra spaces * Add method descriptions and change to use setpoint * Fix build and respond to comments * Remove self from convert_units call * Move method to top * Move method outside class * Add blank lines * Fix test to use farenheit * Update another value to farenheit * Change to celsius * Another test fix * test fix * Fix a value * missed one * Add unit test for convert_units * fix unit test import * Add new line to end of test file * fix convert units import * Reorder imports * Grab const from different import Co-authored-by: Trevor --- homeassistant/components/ozw/climate.py | 51 +++++++++++++++++++++---- tests/components/ozw/test_climate.py | 20 ++++++---- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index a74fd869f0f..67bbe5cdc4d 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -28,6 +28,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.temperature import convert as convert_temperature from .const import DATA_UNSUBSCRIBE, DOMAIN from .entity import ZWaveDeviceEntity @@ -154,6 +155,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) +def convert_units(units): + """Return units as a farenheit or celsius constant.""" + if units == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): """Representation of a Z-Wave Climate device.""" @@ -199,16 +207,18 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def temperature_unit(self): """Return the unit of measurement.""" - if self.values.temperature is not None and self.values.temperature.units == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS + return convert_units(self._current_mode_setpoint_values[0].units) @property def current_temperature(self): """Return the current temperature.""" if not self.values.temperature: return None - return self.values.temperature.value + return convert_temperature( + self.values.temperature.value, + convert_units(self._current_mode_setpoint_values[0].units), + self.temperature_unit, + ) @property def hvac_action(self): @@ -236,17 +246,29 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._current_mode_setpoint_values[0].value + return convert_temperature( + self._current_mode_setpoint_values[0].value, + convert_units(self._current_mode_setpoint_values[0].units), + self.temperature_unit, + ) @property def target_temperature_low(self) -> Optional[float]: """Return the lowbound target temperature we try to reach.""" - return self._current_mode_setpoint_values[0].value + return convert_temperature( + self._current_mode_setpoint_values[0].value, + convert_units(self._current_mode_setpoint_values[0].units), + self.temperature_unit, + ) @property def target_temperature_high(self) -> Optional[float]: """Return the highbound target temperature we try to reach.""" - return self._current_mode_setpoint_values[1].value + return convert_temperature( + self._current_mode_setpoint_values[1].value, + convert_units(self._current_mode_setpoint_values[1].units), + self.temperature_unit, + ) async def async_set_temperature(self, **kwargs): """Set new target temperature. @@ -262,14 +284,29 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): setpoint = self._current_mode_setpoint_values[0] target_temp = kwargs.get(ATTR_TEMPERATURE) if setpoint is not None and target_temp is not None: + target_temp = convert_temperature( + target_temp, + self.temperature_unit, + convert_units(setpoint.units), + ) setpoint.send_value(target_temp) elif len(self._current_mode_setpoint_values) == 2: (setpoint_low, setpoint_high) = self._current_mode_setpoint_values target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) if setpoint_low is not None and target_temp_low is not None: + target_temp_low = convert_temperature( + target_temp_low, + self.temperature_unit, + convert_units(setpoint_low.units), + ) setpoint_low.send_value(target_temp_low) if setpoint_high is not None and target_temp_high is not None: + target_temp_high = convert_temperature( + target_temp_high, + self.temperature_unit, + convert_units(setpoint_high.units), + ) setpoint_high.send_value(target_temp_high) async def async_set_fan_mode(self, fan_mode): diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py index 3414e6c4832..e251a93c115 100644 --- a/tests/components/ozw/test_climate.py +++ b/tests/components/ozw/test_climate.py @@ -16,6 +16,8 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, ) +from homeassistant.components.ozw.climate import convert_units +from homeassistant.const import TEMP_FAHRENHEIT from .common import setup_ozw @@ -36,8 +38,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): HVAC_MODE_HEAT_COOL, ] assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1 - assert state.attributes[ATTR_TEMPERATURE] == 21.1 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 73.5 + assert state.attributes[ATTR_TEMPERATURE] == 70.0 assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None assert state.attributes[ATTR_FAN_MODE] == "Auto Low" @@ -54,7 +56,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 78.98 + assert round(msg["payload"]["Value"], 2) == 26.1 assert msg["payload"]["ValueIDKey"] == 281475099443218 # Test hvac_mode with set_temperature @@ -72,7 +74,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 75.38 + assert round(msg["payload"]["Value"], 2) == 24.1 assert msg["payload"]["ValueIDKey"] == 281475099443218 # Test set mode @@ -127,8 +129,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): assert state is not None assert state.state == HVAC_MODE_HEAT_COOL assert state.attributes.get(ATTR_TEMPERATURE) is None - assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1 - assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6 + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 70.0 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 78.0 # Test setting high/low temp on multiple setpoints await hass.services.async_call( @@ -144,11 +146,11 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): assert len(sent_messages) == 7 # 2 messages ! msg = sent_messages[-2] # low setpoint assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 68.0 + assert round(msg["payload"]["Value"], 2) == 20.0 assert msg["payload"]["ValueIDKey"] == 281475099443218 msg = sent_messages[-1] # high setpoint assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 77.0 + assert round(msg["payload"]["Value"], 2) == 25.0 assert msg["payload"]["ValueIDKey"] == 562950076153874 # Test basic/single-setpoint thermostat (node 16 in dump) @@ -325,3 +327,5 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): ) assert len(sent_messages) == 12 assert "does not support setting a mode" in caplog.text + + assert convert_units("F") == TEMP_FAHRENHEIT From 42606cedef27adb0197623a897624bf684628ed0 Mon Sep 17 00:00:00 2001 From: Niccolo Zapponi Date: Wed, 3 Feb 2021 18:18:31 +0000 Subject: [PATCH 02/14] Add support for iCloud 2FA (#45818) * Add support for iCloud 2FA * Updated dependency for iCloud * Updated dependency and logic fix * Added logic for handling incorrect 2FA code * Bug fix on failing test * Added myself to codeowners * Added check for 2FA on setup * Updated error message --- CODEOWNERS | 2 +- homeassistant/components/icloud/account.py | 34 ++++--- .../components/icloud/config_flow.py | 54 ++++++++--- homeassistant/components/icloud/const.py | 2 +- homeassistant/components/icloud/manifest.json | 4 +- homeassistant/components/icloud/strings.json | 2 +- .../components/icloud/translations/en.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/icloud/test_config_flow.py | 90 +++++++++++++++++++ 10 files changed, 164 insertions(+), 30 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b8175614fb5..ae62631dcea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -211,7 +211,7 @@ homeassistant/components/hydrawise/* @ptcryan homeassistant/components/hyperion/* @dermotduffy homeassistant/components/iammeter/* @lewei50 homeassistant/components/iaqualink/* @flz -homeassistant/components/icloud/* @Quentame +homeassistant/components/icloud/* @Quentame @nzapponi homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/image/* @home-assistant/core homeassistant/components/incomfort/* @zxdavb diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index e6337085e04..4221cf635ba 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -113,6 +113,12 @@ class IcloudAccount: self._icloud_dir.path, with_family=self._with_family, ) + + if not self.api.is_trusted_session or self.api.requires_2fa: + # Session is no longer trusted + # Trigger a new log in to ensure the user enters the 2FA code again. + raise PyiCloudFailedLoginException + except PyiCloudFailedLoginException: self.api = None # Login failed which means credentials need to be updated. @@ -125,16 +131,7 @@ class IcloudAccount: self._config_entry.data[CONF_USERNAME], ) - self.hass.add_job( - self.hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={ - **self._config_entry.data, - "unique_id": self._config_entry.unique_id, - }, - ) - ) + self._require_reauth() return try: @@ -165,6 +162,10 @@ class IcloudAccount: if self.api is None: return + if not self.api.is_trusted_session or self.api.requires_2fa: + self._require_reauth() + return + api_devices = {} try: api_devices = self.api.devices @@ -228,6 +229,19 @@ class IcloudAccount: utcnow() + timedelta(minutes=self._fetch_interval), ) + def _require_reauth(self): + """Require the user to log in again.""" + self.hass.add_job( + self.hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={ + **self._config_entry.data, + "unique_id": self._config_entry.unique_id, + }, + ) + ) + def _determine_interval(self) -> int: """Calculate new interval between two API fetch (in minutes).""" intervals = {"default": self._max_interval} diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index d447790e432..c79024c4f64 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -125,6 +125,9 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {CONF_PASSWORD: "invalid_auth"} return self._show_setup_form(user_input, errors, step_id) + if self.api.requires_2fa: + return await self.async_step_verification_code() + if self.api.requires_2sa: return await self.async_step_trusted_device() @@ -243,22 +246,29 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_verification_code(self, user_input=None): + async def async_step_verification_code(self, user_input=None, errors=None): """Ask the verification code to the user.""" - errors = {} + if errors is None: + errors = {} if user_input is None: - return await self._show_verification_code_form(user_input) + return await self._show_verification_code_form(user_input, errors) self._verification_code = user_input[CONF_VERIFICATION_CODE] try: - if not await self.hass.async_add_executor_job( - self.api.validate_verification_code, - self._trusted_device, - self._verification_code, - ): - raise PyiCloudException("The code you entered is not valid.") + if self.api.requires_2fa: + if not await self.hass.async_add_executor_job( + self.api.validate_2fa_code, self._verification_code + ): + raise PyiCloudException("The code you entered is not valid.") + else: + if not await self.hass.async_add_executor_job( + self.api.validate_verification_code, + self._trusted_device, + self._verification_code, + ): + raise PyiCloudException("The code you entered is not valid.") except PyiCloudException as error: # Reset to the initial 2FA state to allow the user to retry _LOGGER.error("Failed to verify verification code: %s", error) @@ -266,7 +276,27 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._verification_code = None errors["base"] = "validate_verification_code" - return await self.async_step_trusted_device(None, errors) + if self.api.requires_2fa: + try: + self.api = await self.hass.async_add_executor_job( + PyiCloudService, + self._username, + self._password, + self.hass.helpers.storage.Store( + STORAGE_VERSION, STORAGE_KEY + ).path, + True, + None, + self._with_family, + ) + return await self.async_step_verification_code(None, errors) + except PyiCloudFailedLoginException as error: + _LOGGER.error("Error logging into iCloud service: %s", error) + self.api = None + errors = {CONF_PASSWORD: "invalid_auth"} + return self._show_setup_form(user_input, errors, "user") + else: + return await self.async_step_trusted_device(None, errors) return await self.async_step_user( { @@ -278,11 +308,11 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def _show_verification_code_form(self, user_input=None): + async def _show_verification_code_form(self, user_input=None, errors=None): """Show the verification_code form to the user.""" return self.async_show_form( step_id=CONF_VERIFICATION_CODE, data_schema=vol.Schema({vol.Required(CONF_VERIFICATION_CODE): str}), - errors=None, + errors=errors or {}, ) diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index d62bacf1212..58c62f8a868 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -12,7 +12,7 @@ DEFAULT_GPS_ACCURACY_THRESHOLD = 500 # meters # to store the cookie STORAGE_KEY = DOMAIN -STORAGE_VERSION = 1 +STORAGE_VERSION = 2 PLATFORMS = ["device_tracker", "sensor"] diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 40b58cbf2d0..4d96f42b8cb 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -3,6 +3,6 @@ "name": "Apple iCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", - "requirements": ["pyicloud==0.9.7"], - "codeowners": ["@Quentame"] + "requirements": ["pyicloud==0.10.2"], + "codeowners": ["@Quentame", "@nzapponi"] } diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index d07b3c3b870..70ab11157d3 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -35,7 +35,7 @@ "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "send_verification_code": "Failed to send verification code", - "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" + "validate_verification_code": "Failed to verify your verification code, try again" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/icloud/translations/en.json b/homeassistant/components/icloud/translations/en.json index 3097302ded2..36e657011e3 100644 --- a/homeassistant/components/icloud/translations/en.json +++ b/homeassistant/components/icloud/translations/en.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Invalid authentication", "send_verification_code": "Failed to send verification code", - "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" + "validate_verification_code": "Failed to verify your verification code, try again" }, "step": { "reauth": { diff --git a/requirements_all.txt b/requirements_all.txt index 309b77acfe9..8658e56f706 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,7 +1443,7 @@ pyhomematic==0.1.71 pyhomeworks==0.0.6 # homeassistant.components.icloud -pyicloud==0.9.7 +pyicloud==0.10.2 # homeassistant.components.insteon pyinsteon==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd388ff403c..aa2eb7af342 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -742,7 +742,7 @@ pyheos==0.7.2 pyhomematic==0.1.71 # homeassistant.components.icloud -pyicloud==0.9.7 +pyicloud==0.10.2 # homeassistant.components.insteon pyinsteon==1.0.8 diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index a774e61f3ec..998a69c575a 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -51,6 +51,7 @@ def mock_controller_service(): with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) @@ -58,15 +59,31 @@ def mock_controller_service(): yield service_mock +@pytest.fixture(name="service_2fa") +def mock_controller_2fa_service(): + """Mock a successful 2fa service.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = True + service_mock.return_value.requires_2sa = True + service_mock.return_value.validate_2fa_code = Mock(return_value=True) + service_mock.return_value.is_trusted_session = False + yield service_mock + + @pytest.fixture(name="service_authenticated") def mock_controller_service_authenticated(): """Mock a successful service while already authenticate.""" with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = False + service_mock.return_value.is_trusted_session = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) + service_mock.return_value.validate_2fa_code = Mock(return_value=True) service_mock.return_value.validate_verification_code = Mock(return_value=True) yield service_mock @@ -77,6 +94,7 @@ def mock_controller_service_authenticated_no_device(): with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = False service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) @@ -85,24 +103,53 @@ def mock_controller_service_authenticated_no_device(): yield service_mock +@pytest.fixture(name="service_authenticated_not_trusted") +def mock_controller_service_authenticated_not_trusted(): + """Mock a successful service while already authenticated, but the session is not trusted.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = False + service_mock.return_value.requires_2sa = False + service_mock.return_value.is_trusted_session = False + service_mock.return_value.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_code = Mock(return_value=True) + service_mock.return_value.validate_2fa_code = Mock(return_value=True) + service_mock.return_value.validate_verification_code = Mock(return_value=True) + yield service_mock + + @pytest.fixture(name="service_send_verification_code_failed") def mock_controller_service_send_verification_code_failed(): """Mock a failed service during sending verification code step.""" with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=False) yield service_mock +@pytest.fixture(name="service_validate_2fa_code_failed") +def mock_controller_service_validate_2fa_code_failed(): + """Mock a failed service during validation of 2FA verification code step.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = True + service_mock.return_value.validate_2fa_code = Mock(return_value=False) + yield service_mock + + @pytest.fixture(name="service_validate_verification_code_failed") def mock_controller_service_validate_verification_code_failed(): """Mock a failed service during validation of verification code step.""" with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: + service_mock.return_value.requires_2fa = False service_mock.return_value.requires_2sa = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) @@ -409,6 +456,49 @@ async def test_validate_verification_code_failed( assert result["errors"] == {"base": "validate_verification_code"} +async def test_2fa_code_success(hass: HomeAssistantType, service_2fa: MagicMock): + """Test 2fa step success.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + service_2fa.return_value.requires_2fa = False + service_2fa.return_value.requires_2sa = False + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_VERIFICATION_CODE: "0"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == USERNAME + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_WITH_FAMILY] == DEFAULT_WITH_FAMILY + assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD + + +async def test_validate_2fa_code_failed( + hass: HomeAssistantType, service_validate_2fa_code_failed: MagicMock +): + """Test when we have errors during validate_verification_code.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_VERIFICATION_CODE: "0"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_VERIFICATION_CODE + assert result["errors"] == {"base": "validate_verification_code"} + + async def test_password_update( hass: HomeAssistantType, service_authenticated: MagicMock ): From b98d55f1c7b89281bfcc203e74075f6ea1d34b74 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 4 Feb 2021 14:18:51 +0100 Subject: [PATCH 03/14] Don't log missing mpd artwork inappropriately (#45908) This can get unnecessarily spammy and doesn't represent an actual actionable issue. Fixes: #45235 --- homeassistant/components/mpd/manifest.json | 2 +- homeassistant/components/mpd/media_player.py | 18 ++++++++++-------- requirements_all.txt | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index 12ce5b61b74..a11b9fedd80 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -2,6 +2,6 @@ "domain": "mpd", "name": "Music Player Daemon (MPD)", "documentation": "https://www.home-assistant.io/integrations/mpd", - "requirements": ["python-mpd2==3.0.3"], + "requirements": ["python-mpd2==3.0.4"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 6685347b3e3..371d2060680 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -281,20 +281,22 @@ class MpdDevice(MediaPlayerEntity): try: response = await self._client.readpicture(file) except mpd.CommandError as error: - _LOGGER.warning( - "Retrieving artwork through `readpicture` command failed: %s", - error, - ) + if error.errno is not mpd.FailureResponseCode.NO_EXIST: + _LOGGER.warning( + "Retrieving artwork through `readpicture` command failed: %s", + error, + ) # read artwork contained in the media directory (cover.{jpg,png,tiff,bmp}) if none is embedded if can_albumart and not response: try: response = await self._client.albumart(file) except mpd.CommandError as error: - _LOGGER.warning( - "Retrieving artwork through `albumart` command failed: %s", - error, - ) + if error.errno is not mpd.FailureResponseCode.NO_EXIST: + _LOGGER.warning( + "Retrieving artwork through `albumart` command failed: %s", + error, + ) if not response: return None, None diff --git a/requirements_all.txt b/requirements_all.txt index 8658e56f706..8b5beab64fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1789,7 +1789,7 @@ python-juicenet==1.0.1 python-miio==0.5.4 # homeassistant.components.mpd -python-mpd2==3.0.3 +python-mpd2==3.0.4 # homeassistant.components.mystrom python-mystrom==1.1.2 From 39dd590576faef7231e6a8d9197006d527fbd2d5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 4 Feb 2021 14:16:09 +0100 Subject: [PATCH 04/14] Fix entities device_info property in Harmony integration (#45964) --- homeassistant/components/harmony/remote.py | 2 +- homeassistant/components/harmony/switch.py | 5 +++++ tests/components/harmony/conftest.py | 1 + tests/components/harmony/test_config_flow.py | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 8409983789b..9b3d53c21fa 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -161,7 +161,7 @@ class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): @property def device_info(self): """Return device info.""" - self._data.device_info(DOMAIN) + return self._data.device_info(DOMAIN) @property def unique_id(self): diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index 5fae07c431b..2832872c2ef 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -46,6 +46,11 @@ class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): """Return the unique id.""" return f"{self._data.unique_id}-{self._activity}" + @property + def device_info(self): + """Return device info.""" + return self._data.device_info(DOMAIN) + @property def is_on(self): """Return if the current activity is the one for this switch.""" diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py index e758a2795a9..cde8c43fe89 100644 --- a/tests/components/harmony/conftest.py +++ b/tests/components/harmony/conftest.py @@ -49,6 +49,7 @@ class FakeHarmonyClient: self.change_channel = AsyncMock() self.sync = AsyncMock() self._callbacks = callbacks + self.fw_version = "123.456" async def connect(self): """Connect and call the appropriate callbacks.""" diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 52ef71fc8bc..2a7f80d5c2f 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -153,7 +153,7 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} -async def test_options_flow(hass, mock_hc): +async def test_options_flow(hass, mock_hc, mock_write_config): """Test config flow options.""" config_entry = MockConfigEntry( domain=DOMAIN, From 8e531a301facbc2642333e408ce0fb50b62d5977 Mon Sep 17 00:00:00 2001 From: DeadEnd <45110141+DeadEnded@users.noreply.github.com> Date: Thu, 4 Feb 2021 11:02:56 -0500 Subject: [PATCH 05/14] Fix Local Media in Media Browser (#45987) Co-authored-by: Paulus Schoutsen --- homeassistant/components/media_source/local_source.py | 6 +++--- tests/components/media_source/test_local_source.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index d7a2bdfd938..fa62ba48c5f 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -10,7 +10,7 @@ from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant, callback -from homeassistant.util import raise_if_invalid_filename +from homeassistant.util import raise_if_invalid_path from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia @@ -51,7 +51,7 @@ class LocalSource(MediaSource): raise Unresolvable("Unknown source directory.") try: - raise_if_invalid_filename(location) + raise_if_invalid_path(location) except ValueError as err: raise Unresolvable("Invalid path.") from err @@ -192,7 +192,7 @@ class LocalMediaView(HomeAssistantView): ) -> web.FileResponse: """Start a GET request.""" try: - raise_if_invalid_filename(location) + raise_if_invalid_path(location) except ValueError as err: raise web.HTTPBadRequest() from err diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index ad10df7cfd3..e3e2a3f1617 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -23,7 +23,7 @@ async def test_async_browse_media(hass): await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test/not/exist" ) - assert str(excinfo.value) == "Invalid path." + assert str(excinfo.value) == "Path does not exist." # Test browse file with pytest.raises(media_source.BrowseError) as excinfo: From c68847def7db972994c95006b6fe97a7c705c149 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 4 Feb 2021 16:29:41 +0100 Subject: [PATCH 06/14] Bump zwave-js-server-python to 0.17.1 (#45988) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 7df75d7aed2..7083d6c372b 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.17.0"], + "requirements": ["zwave-js-server-python==0.17.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b5beab64fa..b0de3eb79e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2381,4 +2381,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.0 +zwave-js-server-python==0.17.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa2eb7af342..43686d5410f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1194,4 +1194,4 @@ zigpy-znp==0.3.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.0 +zwave-js-server-python==0.17.1 From 2b1093332545187fe241319ff6952940f5124b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 4 Feb 2021 16:45:59 +0100 Subject: [PATCH 07/14] Bump awesomeversion from 21.2.0 to 21.2.2 (#45993) --- homeassistant/package_constraints.txt | 1 + requirements.txt | 1 + setup.py | 1 + 3 files changed, 3 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 31a7414dc3c..adf5cd6088a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,6 +5,7 @@ aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 +awesomeversion==21.2.2 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 diff --git a/requirements.txt b/requirements.txt index c973f4e4030..4a983b0ba70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ aiohttp==3.7.3 astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 +awesomeversion==21.2.2 bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 diff --git a/setup.py b/setup.py index 7f77e3795b4..84b19d15762 100755 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ REQUIRES = [ "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.3.0", + "awesomeversion==21.2.2", "bcrypt==3.1.7", "certifi>=2020.12.5", "ciso8601==2.1.3", From e312ed98c8f494d25fb5973b85016967eb58bf8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Feb 2021 13:36:55 -1000 Subject: [PATCH 08/14] Do not listen for dhcp packets if the filter cannot be setup (#46006) --- homeassistant/components/dhcp/__init__.py | 19 ++++++++++ tests/components/dhcp/test_init.py | 44 +++++++++++++++++++---- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index a71db430da4..9c2c3d3bc35 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -7,6 +7,7 @@ import logging import os import threading +from scapy.arch.common import compile_filter from scapy.config import conf from scapy.error import Scapy_Exception from scapy.layers.dhcp import DHCP @@ -217,6 +218,15 @@ class DHCPWatcher(WatcherBase): ) return + try: + await _async_verify_working_pcap(self.hass, FILTER) + except (Scapy_Exception, ImportError) as ex: + _LOGGER.error( + "Cannot watch for dhcp packets without a functional packet filter: %s", + ex, + ) + return + self._sniffer = AsyncSniffer( filter=FILTER, started_callback=self._started.set, @@ -283,3 +293,12 @@ def _verify_l2socket_creation_permission(): any permission or bind errors. """ conf.L2socket() + + +async def _async_verify_working_pcap(hass, cap_filter): + """Verify we can create a packet filter. + + If we cannot create a filter we will be listening for + all traffic which is too intensive. + """ + await hass.async_add_executor_job(compile_filter, cap_filter) diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 049128248a7..fc24c8201e2 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -280,7 +280,11 @@ async def test_setup_and_stop(hass): ) await hass.async_block_till_done() - with patch("homeassistant.components.dhcp.AsyncSniffer.start") as start_call: + with patch("homeassistant.components.dhcp.AsyncSniffer.start") as start_call, patch( + "homeassistant.components.dhcp._verify_l2socket_creation_permission", + ), patch( + "homeassistant.components.dhcp.compile_filter", + ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -325,21 +329,49 @@ async def test_setup_fails_non_root(hass, caplog): ) await hass.async_block_till_done() - wait_event = threading.Event() - with patch("os.geteuid", return_value=10), patch( "homeassistant.components.dhcp._verify_l2socket_creation_permission", side_effect=Scapy_Exception, ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - wait_event.set() assert "Cannot watch for dhcp packets without root or CAP_NET_RAW" in caplog.text +async def test_setup_fails_with_broken_libpcap(hass, caplog): + """Test we abort if libpcap is missing or broken.""" + + assert await async_setup_component( + hass, + dhcp.DOMAIN, + {}, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.dhcp._verify_l2socket_creation_permission", + ), patch( + "homeassistant.components.dhcp.compile_filter", + side_effect=ImportError, + ) as compile_filter, patch( + "homeassistant.components.dhcp.AsyncSniffer", + ) as async_sniffer: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert compile_filter.called + assert not async_sniffer.called + assert ( + "Cannot watch for dhcp packets without a functional packet filter" + in caplog.text + ) + + async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass): """Test matching based on hostname and macaddress before start.""" hass.states.async_set( From 8cce60b1accd8acd1cdbfa08fb07a5c4e1128b7c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 4 Feb 2021 20:44:40 +0100 Subject: [PATCH 09/14] Bump zwave-js-server-python to 0.17.2 (#46010) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 7083d6c372b..4bd12baa685 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.17.1"], + "requirements": ["zwave-js-server-python==0.17.2"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index b0de3eb79e5..086c444b3d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2381,4 +2381,4 @@ zigpy==0.32.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.1 +zwave-js-server-python==0.17.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43686d5410f..c2ae196767c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1194,4 +1194,4 @@ zigpy-znp==0.3.0 zigpy==0.32.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.17.1 +zwave-js-server-python==0.17.2 From 77887f123ed76916cc4abe416ac74867c6a7d87a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Feb 2021 12:39:44 -1000 Subject: [PATCH 10/14] dhcp does not need promisc mode. Disable it in scapy (#46018) --- homeassistant/components/dhcp/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 9c2c3d3bc35..d33c6159888 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -292,6 +292,8 @@ def _verify_l2socket_creation_permission(): thread so we will not be able to capture any permission or bind errors. """ + # disable scapy promiscuous mode as we do not need it + conf.sniff_promisc = 0 conf.L2socket() From ac0f856378d9364bcb7d1004814555e26bf78ad5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Feb 2021 23:41:52 +0000 Subject: [PATCH 11/14] Bumped version to 2021.2.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 63e11720c1e..08d9dd751db 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 2 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 64ab495318c266cd1bdff758ccb25fdc1e495461 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 5 Feb 2021 10:05:04 +0100 Subject: [PATCH 12/14] Revert aioshelly (#46038) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 923bcdced34..b3511d4f6b0 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.5.3"], + "requirements": ["aioshelly==0.5.1.beta0"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } diff --git a/requirements_all.txt b/requirements_all.txt index 086c444b3d8..49e21a56a05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.3 +aioshelly==0.5.1.beta0 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2ae196767c..21fdf1a48b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.3 +aioshelly==0.5.1.beta0 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From 7c5af4e7e6afa4a5b45d66e5362281e73e0f9bd0 Mon Sep 17 00:00:00 2001 From: obelix05 Date: Fri, 5 Feb 2021 02:31:47 +0100 Subject: [PATCH 13/14] Prevent fritzbox callmonitor phonebook_id 0 from being ignored (#45990) --- homeassistant/components/fritzbox_callmonitor/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox_callmonitor/base.py b/homeassistant/components/fritzbox_callmonitor/base.py index 79f82de95b7..ec62e196855 100644 --- a/homeassistant/components/fritzbox_callmonitor/base.py +++ b/homeassistant/components/fritzbox_callmonitor/base.py @@ -41,7 +41,7 @@ class FritzBoxPhonebook: @Throttle(MIN_TIME_PHONEBOOK_UPDATE) def update_phonebook(self): """Update the phone book dictionary.""" - if not self.phonebook_id: + if self.phonebook_id is None: return self.phonebook_dict = self.fph.get_all_names(self.phonebook_id) From 8e8646b93acb26ce787e88248c3c83479b429be6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 4 Feb 2021 23:32:56 +0100 Subject: [PATCH 14/14] Upgrade holidays to 0.10.5.2 (#46013) --- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 4fb25c766cc..3351d796e93 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,7 +2,7 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.10.4"], + "requirements": ["holidays==0.10.5.2"], "codeowners": ["@fabaff"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 49e21a56a05..56b9f7a331c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.10.4 +holidays==0.10.5.2 # homeassistant.components.frontend home-assistant-frontend==20210127.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21fdf1a48b3..c0a44a96f0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.10.4 +holidays==0.10.5.2 # homeassistant.components.frontend home-assistant-frontend==20210127.7