Merge pull request #34260 from home-assistant/rc

0.108.5
This commit is contained in:
Paulus Schoutsen 2020-04-15 09:45:10 -07:00 committed by GitHub
commit 607b09ccdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 161 additions and 29 deletions

View File

@ -77,7 +77,7 @@ homeassistant/components/counter/* @fabaff
homeassistant/components/cover/* @home-assistant/core homeassistant/components/cover/* @home-assistant/core
homeassistant/components/cpuspeed/* @fabaff homeassistant/components/cpuspeed/* @fabaff
homeassistant/components/cups/* @fabaff homeassistant/components/cups/* @fabaff
homeassistant/components/daikin/* @fredrike @rofrantz homeassistant/components/daikin/* @fredrike
homeassistant/components/darksky/* @fabaff homeassistant/components/darksky/* @fabaff
homeassistant/components/deconz/* @kane610 homeassistant/components/deconz/* @kane610
homeassistant/components/delijn/* @bollewolle homeassistant/components/delijn/* @bollewolle

View File

@ -347,12 +347,17 @@ class AmbientStation:
async def _attempt_connect(self): async def _attempt_connect(self):
"""Attempt to connect to the socket (retrying later on fail).""" """Attempt to connect to the socket (retrying later on fail)."""
try:
async def connect(timestamp=None):
"""Connect."""
await self.client.websocket.connect() await self.client.websocket.connect()
try:
await connect()
except WebsocketError as err: except WebsocketError as err:
_LOGGER.error("Error with the websocket connection: %s", err) _LOGGER.error("Error with the websocket connection: %s", err)
self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480)
async_call_later(self._hass, self._ws_reconnect_delay, self.ws_connect) async_call_later(self._hass, self._ws_reconnect_delay, connect)
async def ws_connect(self): async def ws_connect(self):
"""Register handlers and connect to the websocket.""" """Register handlers and connect to the websocket."""

View File

@ -482,7 +482,7 @@ async def google_assistant_list(hass, connection, msg):
{ {
"entity_id": entity.entity_id, "entity_id": entity.entity_id,
"traits": [trait.name for trait in entity.traits()], "traits": [trait.name for trait in entity.traits()],
"might_2fa": entity.might_2fa(), "might_2fa": entity.might_2fa_traits(),
} }
) )

View File

@ -17,6 +17,7 @@ from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle from homeassistant.util import Throttle
from . import config_flow # noqa: F401 from . import config_flow # noqa: F401
from .const import TIMEOUT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -91,7 +92,7 @@ async def daikin_api_setup(hass, host):
session = hass.helpers.aiohttp_client.async_get_clientsession() session = hass.helpers.aiohttp_client.async_get_clientsession()
try: try:
with timeout(10): with timeout(TIMEOUT):
device = Appliance(host, session) device = Appliance(host, session)
await device.init() await device.init()
except asyncio.TimeoutError: except asyncio.TimeoutError:

View File

@ -84,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Daikin climate based on config_entry.""" """Set up Daikin climate based on config_entry."""
daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id) daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id)
async_add_entities([DaikinClimate(daikin_api)]) async_add_entities([DaikinClimate(daikin_api)], update_before_add=True)
class DaikinClimate(ClimateDevice): class DaikinClimate(ClimateDevice):

View File

@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from .const import KEY_IP, KEY_MAC from .const import KEY_IP, KEY_MAC, TIMEOUT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -38,7 +38,7 @@ class FlowHandler(config_entries.ConfigFlow):
device = Appliance( device = Appliance(
host, self.hass.helpers.aiohttp_client.async_get_clientsession() host, self.hass.helpers.aiohttp_client.async_get_clientsession()
) )
with timeout(10): with timeout(TIMEOUT):
await device.init() await device.init()
except asyncio.TimeoutError: except asyncio.TimeoutError:
return self.async_abort(reason="device_timeout") return self.async_abort(reason="device_timeout")

View File

@ -25,3 +25,5 @@ SENSOR_TYPES = {
KEY_MAC = "mac" KEY_MAC = "mac"
KEY_IP = "ip" KEY_IP = "ip"
TIMEOUT = 60

View File

@ -3,8 +3,7 @@
"name": "Daikin AC", "name": "Daikin AC",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/daikin", "documentation": "https://www.home-assistant.io/integrations/daikin",
"requirements": ["pydaikin==1.6.2"], "requirements": ["pydaikin==1.6.3"],
"dependencies": [], "codeowners": ["@fredrike"],
"codeowners": ["@fredrike", "@rofrantz"],
"quality_scale": "platinum" "quality_scale": "platinum"
} }

View File

@ -372,14 +372,19 @@ class GoogleEntity:
@callback @callback
def might_2fa(self) -> bool: def might_2fa(self) -> bool:
"""Return if the entity might encounter 2FA.""" """Return if the entity might encounter 2FA."""
if not self.config.should_2fa(self.state):
return False
return self.might_2fa_traits()
@callback
def might_2fa_traits(self) -> bool:
"""Return if the entity might encounter 2FA based on just traits."""
state = self.state state = self.state
domain = state.domain domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
device_class = state.attributes.get(ATTR_DEVICE_CLASS) device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if not self.config.should_2fa(state):
return False
return any( return any(
trait.might_2fa(domain, features, device_class) for trait in self.traits() trait.might_2fa(domain, features, device_class) for trait in self.traits()
) )

View File

@ -6,8 +6,10 @@ from pyipp import (
IPP, IPP,
IPPConnectionError, IPPConnectionError,
IPPConnectionUpgradeRequired, IPPConnectionUpgradeRequired,
IPPError,
IPPParseError, IPPParseError,
IPPResponseError, IPPResponseError,
IPPVersionNotSupportedError,
) )
import voluptuous as vol import voluptuous as vol
@ -70,10 +72,16 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
except IPPConnectionUpgradeRequired: except IPPConnectionUpgradeRequired:
return self._show_setup_form({"base": "connection_upgrade"}) return self._show_setup_form({"base": "connection_upgrade"})
except (IPPConnectionError, IPPResponseError): except (IPPConnectionError, IPPResponseError):
_LOGGER.debug("IPP Connection/Response Error", exc_info=True)
return self._show_setup_form({"base": "connection_error"}) return self._show_setup_form({"base": "connection_error"})
except IPPParseError: except IPPParseError:
_LOGGER.exception("IPP Parse Error") _LOGGER.debug("IPP Parse Error", exc_info=True)
return self.async_abort(reason="parse_error") return self.async_abort(reason="parse_error")
except IPPVersionNotSupportedError:
return self.async_abort(reason="ipp_version_error")
except IPPError:
_LOGGER.debug("IPP Error", exc_info=True)
return self.async_abort(reason="ipp_error")
user_input[CONF_UUID] = info[CONF_UUID] user_input[CONF_UUID] = info[CONF_UUID]
@ -111,10 +119,16 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
except IPPConnectionUpgradeRequired: except IPPConnectionUpgradeRequired:
return self.async_abort(reason="connection_upgrade") return self.async_abort(reason="connection_upgrade")
except (IPPConnectionError, IPPResponseError): except (IPPConnectionError, IPPResponseError):
_LOGGER.debug("IPP Connection/Response Error", exc_info=True)
return self.async_abort(reason="connection_error") return self.async_abort(reason="connection_error")
except IPPParseError: except IPPParseError:
_LOGGER.exception("IPP Parse Error") _LOGGER.debug("IPP Parse Error", exc_info=True)
return self.async_abort(reason="parse_error") return self.async_abort(reason="parse_error")
except IPPVersionNotSupportedError:
return self.async_abort(reason="ipp_version_error")
except IPPError:
_LOGGER.debug("IPP Error", exc_info=True)
return self.async_abort(reason="ipp_error")
if info[CONF_UUID] is not None: if info[CONF_UUID] is not None:
self.discovery_info[CONF_UUID] = info[CONF_UUID] self.discovery_info[CONF_UUID] = info[CONF_UUID]

View File

@ -2,7 +2,7 @@
"domain": "ipp", "domain": "ipp",
"name": "Internet Printing Protocol (IPP)", "name": "Internet Printing Protocol (IPP)",
"documentation": "https://www.home-assistant.io/integrations/ipp", "documentation": "https://www.home-assistant.io/integrations/ipp",
"requirements": ["pyipp==0.9.2"], "requirements": ["pyipp==0.10.1"],
"codeowners": ["@ctalkington"], "codeowners": ["@ctalkington"],
"config_flow": true, "config_flow": true,
"quality_scale": "platinum", "quality_scale": "platinum",

View File

@ -27,6 +27,8 @@
"already_configured": "This printer is already configured.", "already_configured": "This printer is already configured.",
"connection_error": "Failed to connect to printer.", "connection_error": "Failed to connect to printer.",
"connection_upgrade": "Failed to connect to printer due to connection upgrade being required.", "connection_upgrade": "Failed to connect to printer due to connection upgrade being required.",
"ipp_error": "Encountered IPP error.",
"ipp_version_error": "IPP version not supported by printer.",
"parse_error": "Failed to parse response from printer." "parse_error": "Failed to parse response from printer."
} }
} }

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 108 MINOR_VERSION = 108
PATCH_VERSION = "4" PATCH_VERSION = "5"
__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, 7, 0) REQUIRED_PYTHON_VER = (3, 7, 0)

View File

@ -1218,7 +1218,7 @@ pycsspeechtts==1.0.3
# pycups==1.9.73 # pycups==1.9.73
# homeassistant.components.daikin # homeassistant.components.daikin
pydaikin==1.6.2 pydaikin==1.6.3
# homeassistant.components.danfoss_air # homeassistant.components.danfoss_air
pydanfossair==0.1.0 pydanfossair==0.1.0
@ -1336,7 +1336,7 @@ pyintesishome==1.7.3
pyipma==2.0.5 pyipma==2.0.5
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.9.2 pyipp==0.10.1
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==0.2.1 pyiqvia==0.2.1

View File

@ -479,7 +479,7 @@ pychromecast==4.2.0
pycoolmasternet==0.0.4 pycoolmasternet==0.0.4
# homeassistant.components.daikin # homeassistant.components.daikin
pydaikin==1.6.2 pydaikin==1.6.3
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==70 pydeconz==70
@ -519,7 +519,7 @@ pyicloud==0.9.6.1
pyipma==2.0.5 pyipma==2.0.5
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.9.2 pyipp==0.10.1
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==0.2.1 pyiqvia==0.2.1

23
rootfs/init Executable file
View File

@ -0,0 +1,23 @@
#!/bin/execlineb -S0
##
## load default PATH (the same that Docker includes if not provided) if it doesn't exist,
## then go ahead with stage1.
## this was motivated due to this issue:
## - https://github.com/just-containers/s6-overlay/issues/108
##
/bin/importas -D /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PATH PATH
export PATH ${PATH}
##
## Skip further init if the user has a given CMD.
## This is to prevent Home Assistant from starting twice if the user
## decided to override/start via the CMD.
##
ifelse { s6-test $# -ne 0 }
{
$@
}
/etc/s6/init/init-stage1 $@

View File

@ -687,20 +687,30 @@ async def test_list_google_entities(hass, hass_ws_client, setup_api, mock_cloud_
entity = GoogleEntity( entity = GoogleEntity(
hass, MockConfig(should_expose=lambda *_: False), State("light.kitchen", "on") hass, MockConfig(should_expose=lambda *_: False), State("light.kitchen", "on")
) )
entity2 = GoogleEntity(
hass,
MockConfig(should_expose=lambda *_: True, should_2fa=lambda *_: False),
State("cover.garage", "open", {"device_class": "garage"}),
)
with patch( with patch(
"homeassistant.components.google_assistant.helpers.async_get_entities", "homeassistant.components.google_assistant.helpers.async_get_entities",
return_value=[entity], return_value=[entity, entity2],
): ):
await client.send_json({"id": 5, "type": "cloud/google_assistant/entities"}) await client.send_json({"id": 5, "type": "cloud/google_assistant/entities"})
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
assert len(response["result"]) == 1 assert len(response["result"]) == 2
assert response["result"][0] == { assert response["result"][0] == {
"entity_id": "light.kitchen", "entity_id": "light.kitchen",
"might_2fa": False, "might_2fa": False,
"traits": ["action.devices.traits.OnOff"], "traits": ["action.devices.traits.OnOff"],
} }
assert response["result"][1] == {
"entity_id": "cover.garage",
"might_2fa": True,
"traits": ["action.devices.traits.OpenClose"],
}
async def test_update_google_entity(hass, hass_ws_client, setup_api, mock_cloud_login): async def test_update_google_entity(hass, hass_ws_client, setup_api, mock_cloud_login):

View File

@ -33,6 +33,7 @@ class MockConfig(helpers.AbstractConfig):
"""Initialize config.""" """Initialize config."""
super().__init__(hass) super().__init__(hass)
self._should_expose = should_expose self._should_expose = should_expose
self._should_2fa = should_2fa
self._secure_devices_pin = secure_devices_pin self._secure_devices_pin = secure_devices_pin
self._entity_config = entity_config or {} self._entity_config = entity_config or {}
self._local_sdk_webhook_id = local_sdk_webhook_id self._local_sdk_webhook_id = local_sdk_webhook_id
@ -73,6 +74,10 @@ class MockConfig(helpers.AbstractConfig):
"""Expose it all.""" """Expose it all."""
return self._should_expose is None or self._should_expose(state) return self._should_expose is None or self._should_expose(state)
def should_2fa(self, state):
"""Expose it all."""
return self._should_2fa is None or self._should_2fa(state)
BASIC_CONFIG = MockConfig() BASIC_CONFIG = MockConfig()

View File

@ -845,10 +845,8 @@ async def test_lock_unlock_unlock(hass):
assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP
# Test with 2FA override # Test with 2FA override
with patch( with patch.object(
"homeassistant.components.google_assistant.helpers" BASIC_CONFIG, "should_2fa", return_value=False,
".AbstractConfig.should_2fa",
return_value=False,
): ):
await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {"lock": False}, {}) await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {"lock": False}, {})
assert len(calls) == 2 assert len(calls) == 2

View File

@ -1,6 +1,6 @@
"""Tests for the IPP config flow.""" """Tests for the IPP config flow."""
import aiohttp import aiohttp
from pyipp import IPPConnectionUpgradeRequired from pyipp import IPPConnectionUpgradeRequired, IPPError
from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
@ -172,6 +172,74 @@ async def test_zeroconf_parse_error(
assert result["reason"] == "parse_error" assert result["reason"] == "parse_error"
async def test_user_ipp_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort the user flow on IPP error."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError)
user_input = MOCK_USER_INPUT.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=user_input,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "ipp_error"
async def test_zeroconf_ipp_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP error."""
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError)
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "ipp_error"
async def test_user_ipp_version_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort user flow on IPP version not supported error."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"),
headers={"Content-Type": "application/ipp"},
)
user_input = {**MOCK_USER_INPUT}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=user_input,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "ipp_version_error"
async def test_zeroconf_ipp_version_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort zeroconf flow on IPP version not supported error."""
aioclient_mock.post(
"http://192.168.1.31:631/ipp/print",
content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"),
headers={"Content-Type": "application/ipp"},
)
discovery_info = {**MOCK_ZEROCONF_IPP_SERVICE_INFO}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "ipp_version_error"
async def test_user_device_exists_abort( async def test_user_device_exists_abort(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None: ) -> None:

Binary file not shown.