Merge pull request #47490 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-03-05 16:22:59 -08:00 committed by GitHub
commit 1145c30c4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 134 additions and 128 deletions

View File

@ -2,7 +2,7 @@
"domain": "amcrest",
"name": "Amcrest",
"documentation": "https://www.home-assistant.io/integrations/amcrest",
"requirements": ["amcrest==1.7.0"],
"requirements": ["amcrest==1.7.1"],
"dependencies": ["ffmpeg"],
"codeowners": ["@pnbruckner"]
}

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20210302.4"
"home-assistant-frontend==20210302.5"
],
"dependencies": [
"api",

View File

@ -19,7 +19,6 @@ from .const import (
CONF_ALLOW_UNREACHABLE,
DEFAULT_ALLOW_HUE_GROUPS,
DEFAULT_ALLOW_UNREACHABLE,
DEFAULT_SCENE_TRANSITION,
LOGGER,
)
from .errors import AuthenticationRequired, CannotConnect
@ -34,9 +33,7 @@ SCENE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
vol.Optional(
ATTR_TRANSITION, default=DEFAULT_SCENE_TRANSITION
): cv.positive_int,
vol.Optional(ATTR_TRANSITION): cv.positive_int,
}
)
# How long should we sleep if the hub is busy
@ -209,7 +206,7 @@ class HueBridge:
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
transition = call.data.get(ATTR_TRANSITION, DEFAULT_SCENE_TRANSITION)
transition = call.data.get(ATTR_TRANSITION)
group = next(
(group for group in self.api.groups.values() if group.name == group_name),

View File

@ -14,8 +14,6 @@ DEFAULT_ALLOW_UNREACHABLE = False
CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
DEFAULT_ALLOW_HUE_GROUPS = False
DEFAULT_SCENE_TRANSITION = 4
GROUP_TYPE_LIGHT_GROUP = "LightGroup"
GROUP_TYPE_ROOM = "Room"
GROUP_TYPE_LUMINAIRE = "Luminaire"

View File

@ -29,7 +29,6 @@ CONF_GATEWAY_TYPE_ALL: List[str] = [
DOMAIN: str = "mysensors"
MYSENSORS_GATEWAY_READY: str = "mysensors_gateway_ready_{}"
MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}"
MYSENSORS_GATEWAYS: str = "mysensors_gateways"
PLATFORM: str = "platform"

View File

@ -26,7 +26,6 @@ from .const import (
CONF_TOPIC_OUT_PREFIX,
CONF_VERSION,
DOMAIN,
MYSENSORS_GATEWAY_READY,
MYSENSORS_GATEWAY_START_TASK,
MYSENSORS_GATEWAYS,
GatewayId,
@ -36,7 +35,7 @@ from .helpers import discover_mysensors_platform, validate_child, validate_node
_LOGGER = logging.getLogger(__name__)
GATEWAY_READY_TIMEOUT = 15.0
GATEWAY_READY_TIMEOUT = 20.0
MQTT_COMPONENT = "mqtt"
@ -64,24 +63,16 @@ async def try_connect(hass: HomeAssistantType, user_input: Dict[str, str]) -> bo
if user_input[CONF_DEVICE] == MQTT_COMPONENT:
return True # dont validate mqtt. mqtt gateways dont send ready messages :(
try:
gateway_ready = asyncio.Future()
gateway_ready = asyncio.Event()
def gateway_ready_callback(msg):
msg_type = msg.gateway.const.MessageType(msg.type)
_LOGGER.debug("Received MySensors msg type %s: %s", msg_type.name, msg)
if msg_type.name != "internal":
return
internal = msg.gateway.const.Internal(msg.sub_type)
if internal.name != "I_GATEWAY_READY":
return
_LOGGER.debug("Received gateway ready")
gateway_ready.set_result(True)
def on_conn_made(_: BaseAsyncGateway) -> None:
gateway_ready.set()
gateway: Optional[BaseAsyncGateway] = await _get_gateway(
hass,
device=user_input[CONF_DEVICE],
version=user_input[CONF_VERSION],
event_callback=gateway_ready_callback,
event_callback=lambda _: None,
persistence_file=None,
baud_rate=user_input.get(CONF_BAUD_RATE),
tcp_port=user_input.get(CONF_TCP_PORT),
@ -92,12 +83,13 @@ async def try_connect(hass: HomeAssistantType, user_input: Dict[str, str]) -> bo
)
if gateway is None:
return False
gateway.on_conn_made = on_conn_made
connect_task = None
try:
connect_task = asyncio.create_task(gateway.start())
with async_timeout.timeout(20):
await gateway_ready
with async_timeout.timeout(GATEWAY_READY_TIMEOUT):
await gateway_ready.wait()
return True
except asyncio.TimeoutError:
_LOGGER.info("Try gateway connect failed with timeout")
@ -280,6 +272,12 @@ async def _gw_start(
hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway
):
"""Start the gateway."""
gateway_ready = asyncio.Event()
def gateway_connected(_: BaseAsyncGateway):
gateway_ready.set()
gateway.on_conn_made = gateway_connected
# Don't use hass.async_create_task to avoid holding up setup indefinitely.
hass.data[DOMAIN][
MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id)
@ -294,21 +292,15 @@ async def _gw_start(
if entry.data[CONF_DEVICE] == MQTT_COMPONENT:
# Gatways connected via mqtt doesn't send gateway ready message.
return
gateway_ready = asyncio.Future()
gateway_ready_key = MYSENSORS_GATEWAY_READY.format(entry.entry_id)
hass.data[DOMAIN][gateway_ready_key] = gateway_ready
try:
with async_timeout.timeout(GATEWAY_READY_TIMEOUT):
await gateway_ready
await gateway_ready.wait()
except asyncio.TimeoutError:
_LOGGER.warning(
"Gateway %s not ready after %s secs so continuing with setup",
"Gateway %s not connected after %s secs so continuing with setup",
entry.data[CONF_DEVICE],
GATEWAY_READY_TIMEOUT,
)
finally:
hass.data[DOMAIN].pop(gateway_ready_key, None)
def _gw_callback_factory(

View File

@ -8,14 +8,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import decorator
from .const import (
CHILD_CALLBACK,
DOMAIN,
MYSENSORS_GATEWAY_READY,
NODE_CALLBACK,
DevId,
GatewayId,
)
from .const import CHILD_CALLBACK, NODE_CALLBACK, DevId, GatewayId
from .device import get_mysensors_devices
from .helpers import discover_mysensors_platform, validate_set_msg
@ -75,20 +68,6 @@ async def handle_sketch_version(
_handle_node_update(hass, gateway_id, msg)
@HANDLERS.register("I_GATEWAY_READY")
async def handle_gateway_ready(
hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
) -> None:
"""Handle an internal gateway ready message.
Set asyncio future result if gateway is ready.
"""
gateway_ready = hass.data[DOMAIN].get(MYSENSORS_GATEWAY_READY.format(gateway_id))
if gateway_ready is None or gateway_ready.cancelled():
return
gateway_ready.set_result(True)
@callback
def _handle_child_update(
hass: HomeAssistantType, gateway_id: GatewayId, validated: Dict[str, List[DevId]]

View File

@ -7,6 +7,7 @@ import voluptuous as vol
from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -49,15 +50,17 @@ async def async_setup_entry(hass, entry, async_add_entities):
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
await data_handler.register_data_class(
CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None
)
if CAMERA_DATA_CLASS_NAME not in data_handler.data:
raise PlatformNotReady
async def get_entities():
"""Retrieve Netatmo entities."""
await data_handler.register_data_class(
CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None
)
data = data_handler.data
if not data.get(CAMERA_DATA_CLASS_NAME):
if not data_handler.data.get(CAMERA_DATA_CLASS_NAME):
return []
data_class = data_handler.data[CAMERA_DATA_CLASS_NAME]
@ -94,24 +97,25 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(await get_entities(), True)
await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None)
platform = entity_platform.current_platform.get()
if data_handler.data[CAMERA_DATA_CLASS_NAME] is not None:
platform.async_register_entity_service(
SERVICE_SET_PERSONS_HOME,
{vol.Required(ATTR_PERSONS): vol.All(cv.ensure_list, [cv.string])},
"_service_set_persons_home",
)
platform.async_register_entity_service(
SERVICE_SET_PERSON_AWAY,
{vol.Optional(ATTR_PERSON): cv.string},
"_service_set_person_away",
)
platform.async_register_entity_service(
SERVICE_SET_CAMERA_LIGHT,
{vol.Required(ATTR_CAMERA_LIGHT_MODE): vol.In(CAMERA_LIGHT_MODES)},
"_service_set_camera_light",
)
platform.async_register_entity_service(
SERVICE_SET_PERSONS_HOME,
{vol.Required(ATTR_PERSONS): vol.All(cv.ensure_list, [cv.string])},
"_service_set_persons_home",
)
platform.async_register_entity_service(
SERVICE_SET_PERSON_AWAY,
{vol.Optional(ATTR_PERSON): cv.string},
"_service_set_person_away",
)
platform.async_register_entity_service(
SERVICE_SET_CAMERA_LIGHT,
{vol.Required(ATTR_CAMERA_LIGHT_MODE): vol.In(CAMERA_LIGHT_MODES)},
"_service_set_camera_light",
)
class NetatmoCamera(NetatmoBase, Camera):

View File

@ -25,6 +25,7 @@ from homeassistant.const import (
TEMP_CELSIUS,
)
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -81,6 +82,7 @@ NETATMO_MAP_PRESET = {
STATE_NETATMO_AWAY: PRESET_AWAY,
STATE_NETATMO_OFF: STATE_NETATMO_OFF,
STATE_NETATMO_MANUAL: STATE_NETATMO_MANUAL,
STATE_NETATMO_HOME: PRESET_SCHEDULE,
}
HVAC_MAP_NETATMO = {
@ -111,8 +113,8 @@ async def async_setup_entry(hass, entry, async_add_entities):
)
home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME)
if not home_data:
return
if HOMEDATA_DATA_CLASS_NAME not in data_handler.data:
raise PlatformNotReady
async def get_entities():
"""Retrieve Netatmo entities."""
@ -151,6 +153,8 @@ async def async_setup_entry(hass, entry, async_add_entities):
async_add_entities(await get_entities(), True)
await data_handler.unregister_data_class(HOMEDATA_DATA_CLASS_NAME, None)
platform = entity_platform.current_platform.get()
if home_data is not None:

View File

@ -129,7 +129,11 @@ class NetatmoDataHandler:
if update_callback:
update_callback()
except (pyatmo.NoDevice, pyatmo.ApiError) as err:
except pyatmo.NoDevice as err:
_LOGGER.debug(err)
self.data[data_class_entry] = None
except pyatmo.ApiError as err:
_LOGGER.debug(err)
async def register_data_class(

View File

@ -31,18 +31,15 @@ async def async_setup_entry(hass, entry, async_add_entities):
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
if CAMERA_DATA_CLASS_NAME not in data_handler.data:
raise PlatformNotReady
async def get_entities():
"""Retrieve Netatmo entities."""
await data_handler.register_data_class(
CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None
)
entities = []
all_cameras = []
if CAMERA_DATA_CLASS_NAME not in data_handler.data:
raise PlatformNotReady
try:
for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values():
for camera in home.values():

View File

@ -20,6 +20,7 @@ from homeassistant.const import (
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.device_registry import async_entries_for_config_entry
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
@ -129,14 +130,25 @@ async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Netatmo weather and homecoach platform."""
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
await data_handler.register_data_class(
WEATHERSTATION_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME, None
)
await data_handler.register_data_class(
HOMECOACH_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME, None
)
async def find_entities(data_class_name):
"""Find all entities."""
await data_handler.register_data_class(data_class_name, data_class_name, None)
if data_class_name not in data_handler.data:
raise PlatformNotReady
all_module_infos = {}
data = data_handler.data
if not data.get(data_class_name):
if data_class_name not in data:
return []
if data[data_class_name] is None:
return []
data_class = data[data_class_name]
@ -174,6 +186,8 @@ async def async_setup_entry(hass, entry, async_add_entities):
NetatmoSensor(data_handler, data_class_name, module, condition)
)
await data_handler.unregister_data_class(data_class_name, None)
return entities
for data_class_name in [

View File

@ -2,7 +2,7 @@
"domain": "opentherm_gw",
"name": "OpenTherm Gateway",
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"requirements": ["pyotgw==1.0b1"],
"requirements": ["pyotgw==1.1b1"],
"codeowners": ["@mvn23"],
"config_flow": true
}

View File

@ -3,7 +3,7 @@
"name": "Philips TV",
"documentation": "https://www.home-assistant.io/integrations/philips_js",
"requirements": [
"ha-philipsjs==2.3.0"
"ha-philipsjs==2.3.1"
],
"codeowners": [
"@elupus"

View File

@ -13,10 +13,9 @@ from homeassistant.exceptions import (
TemplateError,
Unauthorized,
)
from homeassistant.helpers import config_validation as cv, entity
from homeassistant.helpers import config_validation as cv, entity, template
from homeassistant.helpers.event import TrackTemplate, async_track_template_result
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.template import Template
from homeassistant.loader import IntegrationNotFound, async_get_integration
from . import const, decorators, messages
@ -132,6 +131,11 @@ async def handle_call_service(hass, connection, msg):
if msg["domain"] == HASS_DOMAIN and msg["service"] in ["restart", "stop"]:
blocking = False
# We do not support templates.
target = msg.get("target")
if template.is_complex(target):
raise vol.Invalid("Templates are not supported here")
try:
context = connection.context(msg)
await hass.services.async_call(
@ -140,7 +144,7 @@ async def handle_call_service(hass, connection, msg):
msg.get("service_data"),
blocking,
context,
target=msg.get("target"),
target=target,
)
connection.send_message(
messages.result_message(msg["id"], {"context": context})
@ -256,14 +260,14 @@ def handle_ping(hass, connection, msg):
async def handle_render_template(hass, connection, msg):
"""Handle render_template command."""
template_str = msg["template"]
template = Template(template_str, hass)
template_obj = template.Template(template_str, hass)
variables = msg.get("variables")
timeout = msg.get("timeout")
info = None
if timeout:
try:
timed_out = await template.async_render_will_timeout(timeout)
timed_out = await template_obj.async_render_will_timeout(timeout)
except TemplateError as ex:
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
return
@ -294,7 +298,7 @@ async def handle_render_template(hass, connection, msg):
try:
info = async_track_template_result(
hass,
[TrackTemplate(template, variables)],
[TrackTemplate(template_obj, variables)],
_template_listener,
raise_on_template_error=True,
)

View File

@ -3,7 +3,7 @@
"name": "Z-Wave JS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
"requirements": ["zwave-js-server-python==0.21.0"],
"requirements": ["zwave-js-server-python==0.21.1"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["http", "websocket_api"]
}

View File

@ -70,10 +70,15 @@ set_config_parameter:
refresh_value:
name: Refresh value(s) of a Z-Wave entity
description: Force update value(s) for a Z-Wave entity
target:
entity:
integration: zwave_js
fields:
entity_id:
name: Entity
description: Entity whose value(s) should be refreshed
required: true
example: sensor.family_room_motion
selector:
entity:
integration: zwave_js
refresh_all_values:
name: Refresh all values?
description: Whether to refresh all values (true) or just the primary value (false)

View File

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

View File

@ -15,7 +15,7 @@ defusedxml==0.6.0
distro==1.5.0
emoji==1.2.0
hass-nabucasa==0.41.0
home-assistant-frontend==20210302.4
home-assistant-frontend==20210302.5
httpx==0.16.1
jinja2>=2.11.3
netdisco==2.8.2

View File

@ -245,7 +245,7 @@ alpha_vantage==2.3.1
ambiclimate==0.2.1
# homeassistant.components.amcrest
amcrest==1.7.0
amcrest==1.7.1
# homeassistant.components.androidtv
androidtv[async]==0.0.57
@ -721,7 +721,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==2.3.0
ha-philipsjs==2.3.1
# homeassistant.components.habitica
habitipy==0.2.0
@ -763,7 +763,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210302.4
home-assistant-frontend==20210302.5
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -1603,7 +1603,7 @@ pyoppleio==1.0.5
pyota==2.0.5
# homeassistant.components.opentherm_gw
pyotgw==1.0b1
pyotgw==1.1b1
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@ -2397,4 +2397,4 @@ zigpy==0.32.0
zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.21.0
zwave-js-server-python==0.21.1

View File

@ -382,7 +382,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==2.3.0
ha-philipsjs==2.3.1
# homeassistant.components.habitica
habitipy==0.2.0
@ -412,7 +412,7 @@ hole==0.5.1
holidays==0.10.5.2
# homeassistant.components.frontend
home-assistant-frontend==20210302.4
home-assistant-frontend==20210302.5
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -851,7 +851,7 @@ pyopenuv==1.0.9
pyopnsense==0.2.0
# homeassistant.components.opentherm_gw
pyotgw==1.0b1
pyotgw==1.1b1
# homeassistant.auth.mfa_modules.notify
# homeassistant.auth.mfa_modules.totp
@ -1234,4 +1234,4 @@ zigpy-znp==0.4.0
zigpy==0.32.0
# homeassistant.components.zwave_js
zwave-js-server-python==0.21.0
zwave-js-server-python==0.21.1

View File

@ -189,6 +189,7 @@ async def test_hue_activate_scene(hass, mock_api):
assert len(mock_api.mock_requests) == 3
assert mock_api.mock_requests[2]["json"]["scene"] == "scene_1"
assert "transitiontime" not in mock_api.mock_requests[2]["json"]
assert mock_api.mock_requests[2]["path"] == "groups/group_1/action"

View File

@ -21,13 +21,7 @@ from tests.common import MockEntity, MockEntityPlatform, async_mock_service
async def test_call_service(hass, websocket_client):
"""Test call service command."""
calls = []
@callback
def service_call(call):
calls.append(call)
hass.services.async_register("domain_test", "test_service", service_call)
calls = async_mock_service(hass, "domain_test", "test_service")
await websocket_client.send_json(
{
@ -54,13 +48,7 @@ async def test_call_service(hass, websocket_client):
async def test_call_service_target(hass, websocket_client):
"""Test call service command with target."""
calls = []
@callback
def service_call(call):
calls.append(call)
hass.services.async_register("domain_test", "test_service", service_call)
calls = async_mock_service(hass, "domain_test", "test_service")
await websocket_client.send_json(
{
@ -93,6 +81,28 @@ async def test_call_service_target(hass, websocket_client):
}
async def test_call_service_target_template(hass, websocket_client):
"""Test call service command with target does not allow template."""
await websocket_client.send_json(
{
"id": 5,
"type": "call_service",
"domain": "domain_test",
"service": "test_service",
"service_data": {"hello": "world"},
"target": {
"entity_id": "{{ 1 }}",
},
}
)
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert not msg["success"]
assert msg["error"]["code"] == const.ERR_INVALID_FORMAT
async def test_call_service_not_found(hass, websocket_client):
"""Test call service command."""
await websocket_client.send_json(
@ -232,7 +242,6 @@ async def test_call_service_error(hass, websocket_client):
)
msg = await websocket_client.receive_json()
print(msg)
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert msg["success"] is False
@ -249,7 +258,6 @@ async def test_call_service_error(hass, websocket_client):
)
msg = await websocket_client.receive_json()
print(msg)
assert msg["id"] == 6
assert msg["type"] == const.TYPE_RESULT
assert msg["success"] is False