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/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index a71db430da4..d33c6159888 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, @@ -282,4 +292,15 @@ 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() + + +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/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) 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/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/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/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/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/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/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/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 7df75d7aed2..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.0"], + "requirements": ["zwave-js-server-python==0.17.2"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } 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) 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/requirements_all.txt b/requirements_all.txt index 309b77acfe9..56b9f7a331c 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 @@ -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 @@ -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 @@ -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 @@ -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.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd388ff403c..c0a44a96f0a 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 @@ -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 @@ -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 @@ -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.2 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", 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( 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, 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 ): 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: 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