Merge pull request #46023 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-02-05 10:54:07 +01:00 committed by GitHub
commit e4b987a642
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 317 additions and 78 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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."""

View File

@ -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}

View File

@ -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 {},
)

View File

@ -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"]

View File

@ -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"]
}

View File

@ -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%]",

View File

@ -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": {

View File

@ -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

View File

@ -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"]
}

View File

@ -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

View File

@ -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):

View File

@ -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"]
}

View File

@ -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"
}

View File

@ -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"]
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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(

View File

@ -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."""

View File

@ -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,

View File

@ -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
):

View File

@ -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:

View File

@ -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