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/hyperion/* @dermotduffy
homeassistant/components/iammeter/* @lewei50 homeassistant/components/iammeter/* @lewei50
homeassistant/components/iaqualink/* @flz homeassistant/components/iaqualink/* @flz
homeassistant/components/icloud/* @Quentame homeassistant/components/icloud/* @Quentame @nzapponi
homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/ign_sismologia/* @exxamalte
homeassistant/components/image/* @home-assistant/core homeassistant/components/image/* @home-assistant/core
homeassistant/components/incomfort/* @zxdavb homeassistant/components/incomfort/* @zxdavb

View File

@ -7,6 +7,7 @@ import logging
import os import os
import threading import threading
from scapy.arch.common import compile_filter
from scapy.config import conf from scapy.config import conf
from scapy.error import Scapy_Exception from scapy.error import Scapy_Exception
from scapy.layers.dhcp import DHCP from scapy.layers.dhcp import DHCP
@ -217,6 +218,15 @@ class DHCPWatcher(WatcherBase):
) )
return 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( self._sniffer = AsyncSniffer(
filter=FILTER, filter=FILTER,
started_callback=self._started.set, started_callback=self._started.set,
@ -282,4 +292,15 @@ def _verify_l2socket_creation_permission():
thread so we will not be able to capture thread so we will not be able to capture
any permission or bind errors. any permission or bind errors.
""" """
# disable scapy promiscuous mode as we do not need it
conf.sniff_promisc = 0
conf.L2socket() 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) @Throttle(MIN_TIME_PHONEBOOK_UPDATE)
def update_phonebook(self): def update_phonebook(self):
"""Update the phone book dictionary.""" """Update the phone book dictionary."""
if not self.phonebook_id: if self.phonebook_id is None:
return return
self.phonebook_dict = self.fph.get_all_names(self.phonebook_id) self.phonebook_dict = self.fph.get_all_names(self.phonebook_id)

View File

@ -161,7 +161,7 @@ class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity):
@property @property
def device_info(self): def device_info(self):
"""Return device info.""" """Return device info."""
self._data.device_info(DOMAIN) return self._data.device_info(DOMAIN)
@property @property
def unique_id(self): def unique_id(self):

View File

@ -46,6 +46,11 @@ class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity):
"""Return the unique id.""" """Return the unique id."""
return f"{self._data.unique_id}-{self._activity}" return f"{self._data.unique_id}-{self._activity}"
@property
def device_info(self):
"""Return device info."""
return self._data.device_info(DOMAIN)
@property @property
def is_on(self): def is_on(self):
"""Return if the current activity is the one for this switch.""" """Return if the current activity is the one for this switch."""

View File

@ -113,6 +113,12 @@ class IcloudAccount:
self._icloud_dir.path, self._icloud_dir.path,
with_family=self._with_family, 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: except PyiCloudFailedLoginException:
self.api = None self.api = None
# Login failed which means credentials need to be updated. # Login failed which means credentials need to be updated.
@ -125,16 +131,7 @@ class IcloudAccount:
self._config_entry.data[CONF_USERNAME], self._config_entry.data[CONF_USERNAME],
) )
self.hass.add_job( self._require_reauth()
self.hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_REAUTH},
data={
**self._config_entry.data,
"unique_id": self._config_entry.unique_id,
},
)
)
return return
try: try:
@ -165,6 +162,10 @@ class IcloudAccount:
if self.api is None: if self.api is None:
return return
if not self.api.is_trusted_session or self.api.requires_2fa:
self._require_reauth()
return
api_devices = {} api_devices = {}
try: try:
api_devices = self.api.devices api_devices = self.api.devices
@ -228,6 +229,19 @@ class IcloudAccount:
utcnow() + timedelta(minutes=self._fetch_interval), 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: def _determine_interval(self) -> int:
"""Calculate new interval between two API fetch (in minutes).""" """Calculate new interval between two API fetch (in minutes)."""
intervals = {"default": self._max_interval} intervals = {"default": self._max_interval}

View File

@ -125,6 +125,9 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {CONF_PASSWORD: "invalid_auth"} errors = {CONF_PASSWORD: "invalid_auth"}
return self._show_setup_form(user_input, errors, step_id) 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: if self.api.requires_2sa:
return await self.async_step_trusted_device() return await self.async_step_trusted_device()
@ -243,22 +246,29 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors or {}, 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.""" """Ask the verification code to the user."""
errors = {} if errors is None:
errors = {}
if user_input is None: 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] self._verification_code = user_input[CONF_VERIFICATION_CODE]
try: try:
if not await self.hass.async_add_executor_job( if self.api.requires_2fa:
self.api.validate_verification_code, if not await self.hass.async_add_executor_job(
self._trusted_device, self.api.validate_2fa_code, self._verification_code
self._verification_code, ):
): raise PyiCloudException("The code you entered is not valid.")
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: except PyiCloudException as error:
# Reset to the initial 2FA state to allow the user to retry # Reset to the initial 2FA state to allow the user to retry
_LOGGER.error("Failed to verify verification code: %s", error) _LOGGER.error("Failed to verify verification code: %s", error)
@ -266,7 +276,27 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._verification_code = None self._verification_code = None
errors["base"] = "validate_verification_code" 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( 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.""" """Show the verification_code form to the user."""
return self.async_show_form( return self.async_show_form(
step_id=CONF_VERIFICATION_CODE, step_id=CONF_VERIFICATION_CODE,
data_schema=vol.Schema({vol.Required(CONF_VERIFICATION_CODE): str}), 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 # to store the cookie
STORAGE_KEY = DOMAIN STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1 STORAGE_VERSION = 2
PLATFORMS = ["device_tracker", "sensor"] PLATFORMS = ["device_tracker", "sensor"]

View File

@ -3,6 +3,6 @@
"name": "Apple iCloud", "name": "Apple iCloud",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/icloud", "documentation": "https://www.home-assistant.io/integrations/icloud",
"requirements": ["pyicloud==0.9.7"], "requirements": ["pyicloud==0.10.2"],
"codeowners": ["@Quentame"] "codeowners": ["@Quentame", "@nzapponi"]
} }

View File

@ -35,7 +35,7 @@
"error": { "error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"send_verification_code": "Failed to send verification code", "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": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",

View File

@ -8,7 +8,7 @@
"error": { "error": {
"invalid_auth": "Invalid authentication", "invalid_auth": "Invalid authentication",
"send_verification_code": "Failed to send verification code", "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": { "step": {
"reauth": { "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_player.errors import BrowseError
from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.error import Unresolvable
from homeassistant.core import HomeAssistant, callback 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 .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
@ -51,7 +51,7 @@ class LocalSource(MediaSource):
raise Unresolvable("Unknown source directory.") raise Unresolvable("Unknown source directory.")
try: try:
raise_if_invalid_filename(location) raise_if_invalid_path(location)
except ValueError as err: except ValueError as err:
raise Unresolvable("Invalid path.") from err raise Unresolvable("Invalid path.") from err
@ -192,7 +192,7 @@ class LocalMediaView(HomeAssistantView):
) -> web.FileResponse: ) -> web.FileResponse:
"""Start a GET request.""" """Start a GET request."""
try: try:
raise_if_invalid_filename(location) raise_if_invalid_path(location)
except ValueError as err: except ValueError as err:
raise web.HTTPBadRequest() from err raise web.HTTPBadRequest() from err

View File

@ -2,6 +2,6 @@
"domain": "mpd", "domain": "mpd",
"name": "Music Player Daemon (MPD)", "name": "Music Player Daemon (MPD)",
"documentation": "https://www.home-assistant.io/integrations/mpd", "documentation": "https://www.home-assistant.io/integrations/mpd",
"requirements": ["python-mpd2==3.0.3"], "requirements": ["python-mpd2==3.0.4"],
"codeowners": ["@fabaff"] "codeowners": ["@fabaff"]
} }

View File

@ -281,20 +281,22 @@ class MpdDevice(MediaPlayerEntity):
try: try:
response = await self._client.readpicture(file) response = await self._client.readpicture(file)
except mpd.CommandError as error: except mpd.CommandError as error:
_LOGGER.warning( if error.errno is not mpd.FailureResponseCode.NO_EXIST:
"Retrieving artwork through `readpicture` command failed: %s", _LOGGER.warning(
error, "Retrieving artwork through `readpicture` command failed: %s",
) error,
)
# read artwork contained in the media directory (cover.{jpg,png,tiff,bmp}) if none is embedded # read artwork contained in the media directory (cover.{jpg,png,tiff,bmp}) if none is embedded
if can_albumart and not response: if can_albumart and not response:
try: try:
response = await self._client.albumart(file) response = await self._client.albumart(file)
except mpd.CommandError as error: except mpd.CommandError as error:
_LOGGER.warning( if error.errno is not mpd.FailureResponseCode.NO_EXIST:
"Retrieving artwork through `albumart` command failed: %s", _LOGGER.warning(
error, "Retrieving artwork through `albumart` command failed: %s",
) error,
)
if not response: if not response:
return None, None 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.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.temperature import convert as convert_temperature
from .const import DATA_UNSUBSCRIBE, DOMAIN from .const import DATA_UNSUBSCRIBE, DOMAIN
from .entity import ZWaveDeviceEntity 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): class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
"""Representation of a Z-Wave Climate device.""" """Representation of a Z-Wave Climate device."""
@ -199,16 +207,18 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
if self.values.temperature is not None and self.values.temperature.units == "F": return convert_units(self._current_mode_setpoint_values[0].units)
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
if not self.values.temperature: if not self.values.temperature:
return None 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 @property
def hvac_action(self): def hvac_action(self):
@ -236,17 +246,29 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """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 @property
def target_temperature_low(self) -> Optional[float]: def target_temperature_low(self) -> Optional[float]:
"""Return the lowbound target temperature we try to reach.""" """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 @property
def target_temperature_high(self) -> Optional[float]: def target_temperature_high(self) -> Optional[float]:
"""Return the highbound target temperature we try to reach.""" """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): async def async_set_temperature(self, **kwargs):
"""Set new target temperature. """Set new target temperature.
@ -262,14 +284,29 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
setpoint = self._current_mode_setpoint_values[0] setpoint = self._current_mode_setpoint_values[0]
target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp = kwargs.get(ATTR_TEMPERATURE)
if setpoint is not None and target_temp is not None: 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) setpoint.send_value(target_temp)
elif len(self._current_mode_setpoint_values) == 2: elif len(self._current_mode_setpoint_values) == 2:
(setpoint_low, setpoint_high) = self._current_mode_setpoint_values (setpoint_low, setpoint_high) = self._current_mode_setpoint_values
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if setpoint_low is not None and target_temp_low is not None: 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) setpoint_low.send_value(target_temp_low)
if setpoint_high is not None and target_temp_high is not None: 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) setpoint_high.send_value(target_temp_high)
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode):

View File

@ -3,7 +3,7 @@
"name": "Shelly", "name": "Shelly",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly", "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*" }], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
"codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"]
} }

View File

@ -2,7 +2,7 @@
"domain": "workday", "domain": "workday",
"name": "Workday", "name": "Workday",
"documentation": "https://www.home-assistant.io/integrations/workday", "documentation": "https://www.home-assistant.io/integrations/workday",
"requirements": ["holidays==0.10.4"], "requirements": ["holidays==0.10.5.2"],
"codeowners": ["@fabaff"], "codeowners": ["@fabaff"],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@ -3,7 +3,7 @@
"name": "Z-Wave JS", "name": "Z-Wave JS",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js", "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"], "codeowners": ["@home-assistant/z-wave"],
"dependencies": ["http", "websocket_api"] "dependencies": ["http", "websocket_api"]
} }

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 2021 MAJOR_VERSION = 2021
MINOR_VERSION = 2 MINOR_VERSION = 2
PATCH_VERSION = "0" PATCH_VERSION = "1"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 8, 0) REQUIRED_PYTHON_VER = (3, 8, 0)

View File

@ -5,6 +5,7 @@ aiohttp_cors==0.7.0
astral==1.10.1 astral==1.10.1
async_timeout==3.0.1 async_timeout==3.0.1
attrs==19.3.0 attrs==19.3.0
awesomeversion==21.2.2
bcrypt==3.1.7 bcrypt==3.1.7
certifi>=2020.12.5 certifi>=2020.12.5
ciso8601==2.1.3 ciso8601==2.1.3

View File

@ -5,6 +5,7 @@ aiohttp==3.7.3
astral==1.10.1 astral==1.10.1
async_timeout==3.0.1 async_timeout==3.0.1
attrs==19.3.0 attrs==19.3.0
awesomeversion==21.2.2
bcrypt==3.1.7 bcrypt==3.1.7
certifi>=2020.12.5 certifi>=2020.12.5
ciso8601==2.1.3 ciso8601==2.1.3

View File

@ -221,7 +221,7 @@ aiopylgtv==0.3.3
aiorecollect==1.0.1 aiorecollect==1.0.1
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==0.5.3 aioshelly==0.5.1.beta0
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==1.2.1 aioswitcher==1.2.1
@ -762,7 +762,7 @@ hlk-sw16==0.0.9
hole==0.5.1 hole==0.5.1
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.10.4 holidays==0.10.5.2
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20210127.7 home-assistant-frontend==20210127.7
@ -1443,7 +1443,7 @@ pyhomematic==0.1.71
pyhomeworks==0.0.6 pyhomeworks==0.0.6
# homeassistant.components.icloud # homeassistant.components.icloud
pyicloud==0.9.7 pyicloud==0.10.2
# homeassistant.components.insteon # homeassistant.components.insteon
pyinsteon==1.0.8 pyinsteon==1.0.8
@ -1789,7 +1789,7 @@ python-juicenet==1.0.1
python-miio==0.5.4 python-miio==0.5.4
# homeassistant.components.mpd # homeassistant.components.mpd
python-mpd2==3.0.3 python-mpd2==3.0.4
# homeassistant.components.mystrom # homeassistant.components.mystrom
python-mystrom==1.1.2 python-mystrom==1.1.2
@ -2381,4 +2381,4 @@ zigpy==0.32.0
zm-py==0.5.2 zm-py==0.5.2
# homeassistant.components.zwave_js # 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 aiorecollect==1.0.1
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==0.5.3 aioshelly==0.5.1.beta0
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==1.2.1 aioswitcher==1.2.1
@ -399,7 +399,7 @@ hlk-sw16==0.0.9
hole==0.5.1 hole==0.5.1
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.10.4 holidays==0.10.5.2
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20210127.7 home-assistant-frontend==20210127.7
@ -742,7 +742,7 @@ pyheos==0.7.2
pyhomematic==0.1.71 pyhomematic==0.1.71
# homeassistant.components.icloud # homeassistant.components.icloud
pyicloud==0.9.7 pyicloud==0.10.2
# homeassistant.components.insteon # homeassistant.components.insteon
pyinsteon==1.0.8 pyinsteon==1.0.8
@ -1194,4 +1194,4 @@ zigpy-znp==0.3.0
zigpy==0.32.0 zigpy==0.32.0
# homeassistant.components.zwave_js # 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", "astral==1.10.1",
"async_timeout==3.0.1", "async_timeout==3.0.1",
"attrs==19.3.0", "attrs==19.3.0",
"awesomeversion==21.2.2",
"bcrypt==3.1.7", "bcrypt==3.1.7",
"certifi>=2020.12.5", "certifi>=2020.12.5",
"ciso8601==2.1.3", "ciso8601==2.1.3",

View File

@ -280,7 +280,11 @@ async def test_setup_and_stop(hass):
) )
await hass.async_block_till_done() 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) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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() await hass.async_block_till_done()
wait_event = threading.Event()
with patch("os.geteuid", return_value=10), patch( with patch("os.geteuid", return_value=10), patch(
"homeassistant.components.dhcp._verify_l2socket_creation_permission", "homeassistant.components.dhcp._verify_l2socket_creation_permission",
side_effect=Scapy_Exception, side_effect=Scapy_Exception,
): ):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done() 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 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): async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass):
"""Test matching based on hostname and macaddress before start.""" """Test matching based on hostname and macaddress before start."""
hass.states.async_set( hass.states.async_set(

View File

@ -49,6 +49,7 @@ class FakeHarmonyClient:
self.change_channel = AsyncMock() self.change_channel = AsyncMock()
self.sync = AsyncMock() self.sync = AsyncMock()
self._callbacks = callbacks self._callbacks = callbacks
self.fw_version = "123.456"
async def connect(self): async def connect(self):
"""Connect and call the appropriate callbacks.""" """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"} 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.""" """Test config flow options."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,

View File

@ -51,6 +51,7 @@ def mock_controller_service():
with patch( with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService" "homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock: ) as service_mock:
service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = True service_mock.return_value.requires_2sa = True
service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=True) service_mock.return_value.send_verification_code = Mock(return_value=True)
@ -58,15 +59,31 @@ def mock_controller_service():
yield service_mock 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") @pytest.fixture(name="service_authenticated")
def mock_controller_service_authenticated(): def mock_controller_service_authenticated():
"""Mock a successful service while already authenticate.""" """Mock a successful service while already authenticate."""
with patch( with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService" "homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock: ) as service_mock:
service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = 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.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=True) 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) service_mock.return_value.validate_verification_code = Mock(return_value=True)
yield service_mock yield service_mock
@ -77,6 +94,7 @@ def mock_controller_service_authenticated_no_device():
with patch( with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService" "homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock: ) as service_mock:
service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = False service_mock.return_value.requires_2sa = False
service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=True) 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 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") @pytest.fixture(name="service_send_verification_code_failed")
def mock_controller_service_send_verification_code_failed(): def mock_controller_service_send_verification_code_failed():
"""Mock a failed service during sending verification code step.""" """Mock a failed service during sending verification code step."""
with patch( with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService" "homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock: ) as service_mock:
service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = True service_mock.return_value.requires_2sa = True
service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=False) service_mock.return_value.send_verification_code = Mock(return_value=False)
yield service_mock 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") @pytest.fixture(name="service_validate_verification_code_failed")
def mock_controller_service_validate_verification_code_failed(): def mock_controller_service_validate_verification_code_failed():
"""Mock a failed service during validation of verification code step.""" """Mock a failed service during validation of verification code step."""
with patch( with patch(
"homeassistant.components.icloud.config_flow.PyiCloudService" "homeassistant.components.icloud.config_flow.PyiCloudService"
) as service_mock: ) as service_mock:
service_mock.return_value.requires_2fa = False
service_mock.return_value.requires_2sa = True service_mock.return_value.requires_2sa = True
service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.trusted_devices = TRUSTED_DEVICES
service_mock.return_value.send_verification_code = Mock(return_value=True) 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"} 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( async def test_password_update(
hass: HomeAssistantType, service_authenticated: MagicMock hass: HomeAssistantType, service_authenticated: MagicMock
): ):

View File

@ -23,7 +23,7 @@ async def test_async_browse_media(hass):
await media_source.async_browse_media( await media_source.async_browse_media(
hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test/not/exist" 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 # Test browse file
with pytest.raises(media_source.BrowseError) as excinfo: 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_HEAT_COOL,
HVAC_MODE_OFF, HVAC_MODE_OFF,
) )
from homeassistant.components.ozw.climate import convert_units
from homeassistant.const import TEMP_FAHRENHEIT
from .common import setup_ozw 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, HVAC_MODE_HEAT_COOL,
] ]
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 73.5
assert state.attributes[ATTR_TEMPERATURE] == 21.1 assert state.attributes[ATTR_TEMPERATURE] == 70.0
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None
assert state.attributes[ATTR_FAN_MODE] == "Auto Low" 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] msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["topic"] == "OpenZWave/1/command/setvalue/"
# Celsius is converted to Fahrenheit here! # 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 assert msg["payload"]["ValueIDKey"] == 281475099443218
# Test hvac_mode with set_temperature # 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] msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["topic"] == "OpenZWave/1/command/setvalue/"
# Celsius is converted to Fahrenheit here! # 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 assert msg["payload"]["ValueIDKey"] == 281475099443218
# Test set mode # 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 is not None
assert state.state == HVAC_MODE_HEAT_COOL assert state.state == HVAC_MODE_HEAT_COOL
assert state.attributes.get(ATTR_TEMPERATURE) is None assert state.attributes.get(ATTR_TEMPERATURE) is None
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1 assert state.attributes[ATTR_TARGET_TEMP_LOW] == 70.0
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6 assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 78.0
# Test setting high/low temp on multiple setpoints # Test setting high/low temp on multiple setpoints
await hass.services.async_call( 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 ! assert len(sent_messages) == 7 # 2 messages !
msg = sent_messages[-2] # low setpoint msg = sent_messages[-2] # low setpoint
assert msg["topic"] == "OpenZWave/1/command/setvalue/" 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 assert msg["payload"]["ValueIDKey"] == 281475099443218
msg = sent_messages[-1] # high setpoint msg = sent_messages[-1] # high setpoint
assert msg["topic"] == "OpenZWave/1/command/setvalue/" 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 assert msg["payload"]["ValueIDKey"] == 562950076153874
# Test basic/single-setpoint thermostat (node 16 in dump) # 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 len(sent_messages) == 12
assert "does not support setting a mode" in caplog.text assert "does not support setting a mode" in caplog.text
assert convert_units("F") == TEMP_FAHRENHEIT