mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Merge pull request #46023 from home-assistant/rc
This commit is contained in:
commit
e4b987a642
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
@ -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}
|
||||
|
@ -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 {},
|
||||
)
|
||||
|
@ -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"]
|
||||
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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%]",
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
1
setup.py
1
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",
|
||||
|
@ -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(
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user