Merge pull request #49139 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-04-12 17:45:38 -07:00 committed by GitHub
commit b5548c57fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 356 additions and 291 deletions

View File

@ -133,7 +133,7 @@ class CastOptionsFlowHandler(config_entries.OptionsFlow):
) )
if not bad_cec and not bad_hosts and not bad_uuid: if not bad_cec and not bad_hosts and not bad_uuid:
updated_config = {} updated_config = dict(current_config)
updated_config[CONF_IGNORE_CEC] = ignore_cec updated_config[CONF_IGNORE_CEC] = ignore_cec
updated_config[CONF_KNOWN_HOSTS] = known_hosts updated_config[CONF_KNOWN_HOSTS] = known_hosts
updated_config[CONF_UUID] = wanted_uuid updated_config[CONF_UUID] = wanted_uuid

View File

@ -7,6 +7,7 @@ from datetime import timedelta
import functools as ft import functools as ft
import json import json
import logging import logging
from urllib.parse import quote
import pychromecast import pychromecast
from pychromecast.controllers.homeassistant import HomeAssistantController from pychromecast.controllers.homeassistant import HomeAssistantController
@ -472,7 +473,7 @@ class CastDevice(MediaPlayerEntity):
media_id = async_sign_path( media_id = async_sign_path(
self.hass, self.hass,
refresh_token.id, refresh_token.id,
media_id, quote(media_id),
timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME),
) )

View File

@ -209,8 +209,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
} }
if "id" not in properties: if "id" not in properties:
_LOGGER.warning( # This can happen if the TXT record is received after the PTR record
"HomeKit device %s: id not exposed, in violation of spec", properties # we will wait for the next update in this case
_LOGGER.debug(
"HomeKit device %s: id not exposed; TXT record may have not yet been received",
properties,
) )
return self.async_abort(reason="invalid_properties") return self.async_abort(reason="invalid_properties")

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": [ "requirements": [
"aiohomekit==0.2.60" "aiohomekit==0.2.61"
], ],
"zeroconf": [ "zeroconf": [
"_hap._tcp.local." "_hap._tcp.local."

View File

@ -1,6 +1,7 @@
"""Authentication for HTTP component.""" """Authentication for HTTP component."""
import logging import logging
import secrets import secrets
from urllib.parse import unquote
from aiohttp import hdrs from aiohttp import hdrs
from aiohttp.web import middleware from aiohttp.web import middleware
@ -30,11 +31,16 @@ def async_sign_path(hass, refresh_token_id, path, expiration):
now = dt_util.utcnow() now = dt_util.utcnow()
encoded = jwt.encode( encoded = jwt.encode(
{"iss": refresh_token_id, "path": path, "iat": now, "exp": now + expiration}, {
"iss": refresh_token_id,
"path": unquote(path),
"iat": now,
"exp": now + expiration,
},
secret, secret,
algorithm="HS256", algorithm="HS256",
) )
return f"{path}?{SIGN_QUERY_PARAM}=" f"{encoded.decode()}" return f"{path}?{SIGN_QUERY_PARAM}={encoded.decode()}"
@callback @callback

View File

@ -114,7 +114,7 @@ def _add_log_filter(logger, patterns):
"""Add a Filter to the logger based on a regexp of the filter_str.""" """Add a Filter to the logger based on a regexp of the filter_str."""
def filter_func(logrecord): def filter_func(logrecord):
return not any(p.match(logrecord.getMessage()) for p in patterns) return not any(p.search(logrecord.getMessage()) for p in patterns)
logger.addFilter(filter_func) logger.addFilter(filter_func)

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from time import gmtime, strftime, time from time import localtime, strftime, time
from aiolyric.objects.device import LyricDevice from aiolyric.objects.device import LyricDevice
from aiolyric.objects.location import LyricLocation from aiolyric.objects.location import LyricLocation
@ -82,7 +82,7 @@ SCHEMA_HOLD_TIME = {
vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All(
cv.time_period, cv.time_period,
cv.positive_timedelta, cv.positive_timedelta,
lambda td: strftime("%H:%M:%S", gmtime(time() + td.total_seconds())), lambda td: strftime("%H:%M:%S", localtime(time() + td.total_seconds())),
) )
} }

View File

@ -10,6 +10,7 @@ import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
from homeassistant.util.dt import now
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -59,7 +60,7 @@ def setup(hass, config):
scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds() scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds()
try: try:
cube = MaxCube(host, port) cube = MaxCube(host, port, now=now)
hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval)
except timeout as ex: except timeout as ex:
_LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex))

View File

@ -2,6 +2,6 @@
"domain": "maxcube", "domain": "maxcube",
"name": "eQ-3 MAX!", "name": "eQ-3 MAX!",
"documentation": "https://www.home-assistant.io/integrations/maxcube", "documentation": "https://www.home-assistant.io/integrations/maxcube",
"requirements": ["maxcube-api==0.4.1"], "requirements": ["maxcube-api==0.4.2"],
"codeowners": [] "codeowners": []
} }

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from urllib.parse import quote
import voluptuous as vol import voluptuous as vol
@ -123,7 +124,7 @@ async def websocket_resolve_media(hass, connection, msg):
url = async_sign_path( url = async_sign_path(
hass, hass,
connection.refresh_token_id, connection.refresh_token_id,
url, quote(url),
timedelta(seconds=msg["expires"]), timedelta(seconds=msg["expires"]),
) )

View File

@ -1,6 +1,7 @@
"""Support for MQTT fans.""" """Support for MQTT fans."""
import functools import functools
import logging import logging
import math
import voluptuous as vol import voluptuous as vol
@ -441,7 +442,6 @@ class MqttFan(MqttEntity, FanEntity):
) )
return return
if not self._feature_percentage:
if speed in self._legacy_speeds_list_no_off: if speed in self._legacy_speeds_list_no_off:
self._percentage = ordered_list_item_to_percentage( self._percentage = ordered_list_item_to_percentage(
self._legacy_speeds_list_no_off, speed self._legacy_speeds_list_no_off, speed
@ -592,7 +592,7 @@ class MqttFan(MqttEntity, FanEntity):
This method is a coroutine. This method is a coroutine.
""" """
percentage_payload = int( percentage_payload = math.ceil(
percentage_to_ranged_value(self._speed_range, percentage) percentage_to_ranged_value(self._speed_range, percentage)
) )
mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload) mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload)

View File

@ -1,7 +1,7 @@
{ {
"domain": "nexia", "domain": "nexia",
"name": "Nexia", "name": "Nexia",
"requirements": ["nexia==0.9.5"], "requirements": ["nexia==0.9.6"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"documentation": "https://www.home-assistant.io/integrations/nexia", "documentation": "https://www.home-assistant.io/integrations/nexia",
"config_flow": true, "config_flow": true,

View File

@ -13,4 +13,6 @@ def is_invalid_auth_code(http_status_code):
def percent_conv(val): def percent_conv(val):
"""Convert an actual percentage (0.0-1.0) to 0-100 scale.""" """Convert an actual percentage (0.0-1.0) to 0-100 scale."""
if val is None:
return None
return round(val * 100.0, 1) return round(val * 100.0, 1)

View File

@ -134,8 +134,12 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
async def _notify_task(self): async def _notify_task(self):
while self.api.on and self.api.notify_change_supported: while self.api.on and self.api.notify_change_supported:
if await self.api.notifyChange(130): res = await self.api.notifyChange(130)
if res:
self.async_set_updated_data(None) self.async_set_updated_data(None)
elif res is None:
LOGGER.debug("Aborting notify due to unexpected return")
break
@callback @callback
def _async_notify_stop(self): def _async_notify_stop(self):

View File

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

View File

@ -47,11 +47,13 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool:
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up Roku from a config entry.""" """Set up Roku from a config entry."""
coordinator = hass.data[DOMAIN].get(entry.entry_id)
if not coordinator:
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
await coordinator.async_config_entry_first_refresh()
for platform in PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform) hass.config_entries.async_forward_entry_setup(entry, platform)

View File

@ -195,9 +195,15 @@ class ScreenlogicEntity(CoordinatorEntity):
"""Return device information for the controller.""" """Return device information for the controller."""
controller_type = self.config_data["controller_type"] controller_type = self.config_data["controller_type"]
hardware_type = self.config_data["hardware_type"] hardware_type = self.config_data["hardware_type"]
try:
equipment_model = EQUIPMENT.CONTROLLER_HARDWARE[controller_type][
hardware_type
]
except KeyError:
equipment_model = f"Unknown Model C:{controller_type} H:{hardware_type}"
return { return {
"connections": {(dr.CONNECTION_NETWORK_MAC, self.mac)}, "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac)},
"name": self.gateway_name, "name": self.gateway_name,
"manufacturer": "Pentair", "manufacturer": "Pentair",
"model": EQUIPMENT.CONTROLLER_HARDWARE[controller_type][hardware_type], "model": equipment_model,
} }

View File

@ -118,15 +118,16 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
"""Brightness of light.""" """Brightness of light."""
if self.mode == "color": if self.mode == "color":
if self.control_result: if self.control_result:
brightness = self.control_result["gain"] brightness_pct = self.control_result["gain"]
else: else:
brightness = self.block.gain brightness_pct = self.block.gain
else: else:
if self.control_result: if self.control_result:
brightness = self.control_result["brightness"] brightness_pct = self.control_result["brightness"]
else: else:
brightness = self.block.brightness brightness_pct = self.block.brightness
return int(brightness / 100 * 255)
return round(255 * brightness_pct / 100)
@property @property
def white_value(self) -> int: def white_value(self) -> int:
@ -188,11 +189,11 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
set_mode = None set_mode = None
params = {"turn": "on"} params = {"turn": "on"}
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255)
if hasattr(self.block, "gain"): if hasattr(self.block, "gain"):
params["gain"] = tmp_brightness params["gain"] = brightness_pct
if hasattr(self.block, "brightness"): if hasattr(self.block, "brightness"):
params["brightness"] = tmp_brightness params["brightness"] = brightness_pct
if ATTR_COLOR_TEMP in kwargs: if ATTR_COLOR_TEMP in kwargs:
color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp))

View File

@ -150,6 +150,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
THERMOSTAT_OPERATING_STATE_PROPERTY, THERMOSTAT_OPERATING_STATE_PROPERTY,
command_class=CommandClass.THERMOSTAT_OPERATING_STATE, command_class=CommandClass.THERMOSTAT_OPERATING_STATE,
add_to_watched_value_ids=True, add_to_watched_value_ids=True,
check_all_endpoints=True,
) )
self._current_temp = self.get_zwave_value( self._current_temp = self.get_zwave_value(
THERMOSTAT_CURRENT_TEMP_PROPERTY, THERMOSTAT_CURRENT_TEMP_PROPERTY,

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 = 4 MINOR_VERSION = 4
PATCH_VERSION = "3" PATCH_VERSION = "4"
__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

@ -172,7 +172,7 @@ aioguardian==1.0.4
aioharmony==0.2.7 aioharmony==0.2.7
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==0.2.60 aiohomekit==0.2.61
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@ -721,7 +721,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2 ha-ffmpeg==3.0.2
# homeassistant.components.philips_js # homeassistant.components.philips_js
ha-philipsjs==2.3.2 ha-philipsjs==2.7.0
# homeassistant.components.habitica # homeassistant.components.habitica
habitipy==0.2.0 habitipy==0.2.0
@ -916,7 +916,7 @@ magicseaweed==1.0.3
matrix-client==0.3.2 matrix-client==0.3.2
# homeassistant.components.maxcube # homeassistant.components.maxcube
maxcube-api==0.4.1 maxcube-api==0.4.2
# homeassistant.components.mythicbeastsdns # homeassistant.components.mythicbeastsdns
mbddns==0.1.2 mbddns==0.1.2
@ -986,7 +986,7 @@ netdisco==2.8.2
neurio==0.3.1 neurio==0.3.1
# homeassistant.components.nexia # homeassistant.components.nexia
nexia==0.9.5 nexia==0.9.6
# homeassistant.components.nextcloud # homeassistant.components.nextcloud
nextcloudmonitor==1.1.0 nextcloudmonitor==1.1.0

View File

@ -106,7 +106,7 @@ aioguardian==1.0.4
aioharmony==0.2.7 aioharmony==0.2.7
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==0.2.60 aiohomekit==0.2.61
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@ -382,7 +382,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2 ha-ffmpeg==3.0.2
# homeassistant.components.philips_js # homeassistant.components.philips_js
ha-philipsjs==2.3.2 ha-philipsjs==2.7.0
# homeassistant.components.habitica # homeassistant.components.habitica
habitipy==0.2.0 habitipy==0.2.0
@ -476,7 +476,7 @@ logi_circle==0.2.2
luftdaten==0.6.4 luftdaten==0.6.4
# homeassistant.components.maxcube # homeassistant.components.maxcube
maxcube-api==0.4.1 maxcube-api==0.4.2
# homeassistant.components.mythicbeastsdns # homeassistant.components.mythicbeastsdns
mbddns==0.1.2 mbddns==0.1.2
@ -516,7 +516,7 @@ nessclient==0.9.15
netdisco==2.8.2 netdisco==2.8.2
# homeassistant.components.nexia # homeassistant.components.nexia
nexia==0.9.5 nexia==0.9.6
# homeassistant.components.notify_events # homeassistant.components.notify_events
notify-events==1.0.4 notify-events==1.0.4

View File

@ -0,0 +1,244 @@
"""Tests for the Cast config flow."""
from unittest.mock import ANY, patch
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import cast
from tests.common import MockConfigEntry
async def test_creating_entry_sets_up_media_player(hass):
"""Test setting up Cast loads the media player."""
with patch(
"homeassistant.components.cast.media_player.async_setup_entry",
return_value=True,
) as mock_setup, patch(
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
), patch(
"pychromecast.discovery.stop_discovery"
):
result = await hass.config_entries.flow.async_init(
cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
# Confirmation form
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
@pytest.mark.parametrize("source", ["import", "user", "zeroconf"])
async def test_single_instance(hass, source):
"""Test we only allow a single config flow."""
MockConfigEntry(domain="cast").add_to_hass(hass)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
"cast", context={"source": source}
)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
async def test_user_setup(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
async def test_user_setup_options(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "}
)
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": ["192.168.0.1", "192.168.0.2"],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
async def test_zeroconf_setup(hass):
"""Test we can finish a config flow through zeroconf."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "zeroconf"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
def get_suggested(schema, key):
"""Get suggested value for key in voluptuous schema."""
for k in schema.keys():
if k == key:
if k.description is None or "suggested_value" not in k.description:
return None
return k.description["suggested_value"]
@pytest.mark.parametrize(
"parameter_data",
[
(
"known_hosts",
["192.168.0.10", "192.168.0.11"],
"192.168.0.10,192.168.0.11",
"192.168.0.1, , 192.168.0.2 ",
["192.168.0.1", "192.168.0.2"],
),
(
"uuid",
["bla", "blu"],
"bla,blu",
"foo, , bar ",
["foo", "bar"],
),
(
"ignore_cec",
["cast1", "cast2"],
"cast1,cast2",
"other_cast, , some_cast ",
["other_cast", "some_cast"],
),
],
)
async def test_option_flow(hass, parameter_data):
"""Test config flow options."""
all_parameters = ["ignore_cec", "known_hosts", "uuid"]
parameter, initial, suggested, user_input, updated = parameter_data
data = {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
}
data[parameter] = initial
config_entry = MockConfigEntry(domain="cast", data=data)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Test ignore_cec and uuid options are hidden if advanced options are disabled
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
data_schema = result["data_schema"].schema
assert set(data_schema) == {"known_hosts"}
orig_data = dict(config_entry.data)
# Reconfigure ignore_cec, known_hosts, uuid
context = {"source": "user", "show_advanced_options": True}
result = await hass.config_entries.options.async_init(
config_entry.entry_id, context=context
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
data_schema = result["data_schema"].schema
for other_param in all_parameters:
if other_param == parameter:
continue
assert get_suggested(data_schema, other_param) == ""
assert get_suggested(data_schema, parameter) == suggested
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={parameter: user_input},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] is None
for other_param in all_parameters:
if other_param == parameter:
continue
assert config_entry.data[other_param] == []
assert config_entry.data[parameter] == updated
# Clear known_hosts
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"known_hosts": ""},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] is None
assert config_entry.data == {
**orig_data,
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
}
async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock):
"""Test known hosts is passed to pychromecasts."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"}
)
assert result["type"] == "create_entry"
await hass.async_block_till_done()
config_entry = hass.config_entries.async_entries("cast")[0]
assert castbrowser_mock.start_discovery.call_count == 1
castbrowser_constructor_mock.assert_called_once_with(
ANY, ANY, ["192.168.0.1", "192.168.0.2"]
)
castbrowser_mock.reset_mock()
castbrowser_constructor_mock.reset_mock()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"known_hosts": "192.168.0.11, 192.168.0.12"},
)
await hass.async_block_till_done()
castbrowser_mock.start_discovery.assert_not_called()
castbrowser_constructor_mock.assert_not_called()
castbrowser_mock.host_browser.update_hosts.assert_called_once_with(
["192.168.0.11", "192.168.0.12"]
)

View File

@ -1,39 +1,9 @@
"""Tests for the Cast config flow.""" """Tests for the Cast integration."""
from unittest.mock import ANY, patch from unittest.mock import patch
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import cast from homeassistant.components import cast
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
async def test_creating_entry_sets_up_media_player(hass):
"""Test setting up Cast loads the media player."""
with patch(
"homeassistant.components.cast.media_player.async_setup_entry",
return_value=True,
) as mock_setup, patch(
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
), patch(
"pychromecast.discovery.stop_discovery"
):
result = await hass.config_entries.flow.async_init(
cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
# Confirmation form
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
async def test_import(hass, caplog): async def test_import(hass, caplog):
"""Test that specifying config will create an entry.""" """Test that specifying config will create an entry."""
@ -67,7 +37,7 @@ async def test_import(hass, caplog):
async def test_not_configuring_cast_not_creates_entry(hass): async def test_not_configuring_cast_not_creates_entry(hass):
"""Test that no config will not create an entry.""" """Test that an empty config does not create an entry."""
with patch( with patch(
"homeassistant.components.cast.async_setup_entry", return_value=True "homeassistant.components.cast.async_setup_entry", return_value=True
) as mock_setup: ) as mock_setup:
@ -75,207 +45,3 @@ async def test_not_configuring_cast_not_creates_entry(hass):
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 0 assert len(mock_setup.mock_calls) == 0
@pytest.mark.parametrize("source", ["import", "user", "zeroconf"])
async def test_single_instance(hass, source):
"""Test we only allow a single config flow."""
MockConfigEntry(domain="cast").add_to_hass(hass)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
"cast", context={"source": source}
)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
async def test_user_setup(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
async def test_user_setup_options(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "}
)
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": ["192.168.0.1", "192.168.0.2"],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
async def test_zeroconf_setup(hass):
"""Test we can finish a config flow through zeroconf."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "zeroconf"}
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
users = await hass.auth.async_get_users()
assert len(users) == 1
assert result["type"] == "create_entry"
assert result["result"].data == {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
"user_id": users[0].id, # Home Assistant cast user
}
def get_suggested(schema, key):
"""Get suggested value for key in voluptuous schema."""
for k in schema.keys():
if k == key:
if k.description is None or "suggested_value" not in k.description:
return None
return k.description["suggested_value"]
@pytest.mark.parametrize(
"parameter_data",
[
(
"known_hosts",
["192.168.0.10", "192.168.0.11"],
"192.168.0.10,192.168.0.11",
"192.168.0.1, , 192.168.0.2 ",
["192.168.0.1", "192.168.0.2"],
),
(
"uuid",
["bla", "blu"],
"bla,blu",
"foo, , bar ",
["foo", "bar"],
),
(
"ignore_cec",
["cast1", "cast2"],
"cast1,cast2",
"other_cast, , some_cast ",
["other_cast", "some_cast"],
),
],
)
async def test_option_flow(hass, parameter_data):
"""Test config flow options."""
all_parameters = ["ignore_cec", "known_hosts", "uuid"]
parameter, initial, suggested, user_input, updated = parameter_data
data = {
"ignore_cec": [],
"known_hosts": [],
"uuid": [],
}
data[parameter] = initial
config_entry = MockConfigEntry(domain="cast", data=data)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Test ignore_cec and uuid options are hidden if advanced options are disabled
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
data_schema = result["data_schema"].schema
assert set(data_schema) == {"known_hosts"}
# Reconfigure ignore_cec, known_hosts, uuid
context = {"source": "user", "show_advanced_options": True}
result = await hass.config_entries.options.async_init(
config_entry.entry_id, context=context
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
data_schema = result["data_schema"].schema
for other_param in all_parameters:
if other_param == parameter:
continue
assert get_suggested(data_schema, other_param) == ""
assert get_suggested(data_schema, parameter) == suggested
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={parameter: user_input},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] is None
for other_param in all_parameters:
if other_param == parameter:
continue
assert config_entry.data[other_param] == []
assert config_entry.data[parameter] == updated
# Clear known_hosts
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"known_hosts": ""},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] is None
assert config_entry.data == {"ignore_cec": [], "known_hosts": [], "uuid": []}
async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock):
"""Test known hosts is passed to pychromecasts."""
result = await hass.config_entries.flow.async_init(
"cast", context={"source": "user"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"}
)
assert result["type"] == "create_entry"
await hass.async_block_till_done()
config_entry = hass.config_entries.async_entries("cast")[0]
assert castbrowser_mock.start_discovery.call_count == 1
castbrowser_constructor_mock.assert_called_once_with(
ANY, ANY, ["192.168.0.1", "192.168.0.2"]
)
castbrowser_mock.reset_mock()
castbrowser_constructor_mock.reset_mock()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={"known_hosts": "192.168.0.11, 192.168.0.12"},
)
await hass.async_block_till_done()
castbrowser_mock.start_discovery.assert_not_called()
castbrowser_constructor_mock.assert_not_called()
castbrowser_mock.host_browser.update_hosts.assert_called_once_with(
["192.168.0.11", "192.168.0.12"]
)

View File

@ -42,6 +42,7 @@ async def test_log_filtering(hass, caplog):
"doesntmatchanything", "doesntmatchanything",
".*shouldfilterall.*", ".*shouldfilterall.*",
"^filterthis:.*", "^filterthis:.*",
"in the middle",
], ],
"test.other_filter": [".*otherfilterer"], "test.other_filter": [".*otherfilterer"],
}, },
@ -62,6 +63,7 @@ async def test_log_filtering(hass, caplog):
filter_logger, False, "this line containing shouldfilterall should be filtered" filter_logger, False, "this line containing shouldfilterall should be filtered"
) )
msg_test(filter_logger, True, "this line should not be filtered filterthis:") msg_test(filter_logger, True, "this line should not be filtered filterthis:")
msg_test(filter_logger, False, "this in the middle should be filtered")
msg_test(filter_logger, False, "filterthis: should be filtered") msg_test(filter_logger, False, "filterthis: should be filtered")
msg_test(filter_logger, False, "format string shouldfilter%s", "all") msg_test(filter_logger, False, "format string shouldfilter%s", "all")
msg_test(filter_logger, True, "format string shouldfilter%s", "not") msg_test(filter_logger, True, "format string shouldfilter%s", "not")

View File

@ -10,6 +10,7 @@ import pytest
from homeassistant.components.maxcube import DOMAIN from homeassistant.components.maxcube import DOMAIN
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import now
@pytest.fixture @pytest.fixture
@ -105,5 +106,5 @@ async def cube(hass, hass_config, room, thermostat, wallthermostat, windowshutte
assert await async_setup_component(hass, DOMAIN, hass_config) assert await async_setup_component(hass, DOMAIN, hass_config)
await hass.async_block_till_done() await hass.async_block_till_done()
gateway = hass_config[DOMAIN]["gateways"][0] gateway = hass_config[DOMAIN]["gateways"][0]
mock.assert_called_with(gateway["host"], gateway.get("port", 62910)) mock.assert_called_with(gateway["host"], gateway.get("port", 62910), now=now)
return cube return cube

View File

@ -1,5 +1,6 @@
"""Test Media Source initialization.""" """Test Media Source initialization."""
from unittest.mock import patch from unittest.mock import patch
from urllib.parse import quote
import pytest import pytest
@ -45,7 +46,7 @@ async def test_async_browse_media(hass):
media = await media_source.async_browse_media(hass, "") media = await media_source.async_browse_media(hass, "")
assert isinstance(media, media_source.models.BrowseMediaSource) assert isinstance(media, media_source.models.BrowseMediaSource)
assert media.title == "media/" assert media.title == "media/"
assert len(media.children) == 1 assert len(media.children) == 2
# Test invalid media content # Test invalid media content
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -133,14 +134,15 @@ async def test_websocket_browse_media(hass, hass_ws_client):
assert msg["error"]["message"] == "test" assert msg["error"]["message"] == "test"
async def test_websocket_resolve_media(hass, hass_ws_client): @pytest.mark.parametrize("filename", ["test.mp3", "Epic Sax Guy 10 Hours.mp4"])
async def test_websocket_resolve_media(hass, hass_ws_client, filename):
"""Test browse media websocket.""" """Test browse media websocket."""
assert await async_setup_component(hass, const.DOMAIN, {}) assert await async_setup_component(hass, const.DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
media = media_source.models.PlayMedia("/media/local/test.mp3", "audio/mpeg") media = media_source.models.PlayMedia(f"/media/local/{filename}", "audio/mpeg")
with patch( with patch(
"homeassistant.components.media_source.async_resolve_media", "homeassistant.components.media_source.async_resolve_media",
@ -150,7 +152,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client):
{ {
"id": 1, "id": 1,
"type": "media_source/resolve_media", "type": "media_source/resolve_media",
"media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3", "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/{filename}",
} }
) )
@ -158,7 +160,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client):
assert msg["success"] assert msg["success"]
assert msg["id"] == 1 assert msg["id"] == 1
assert msg["result"]["url"].startswith(media.url) assert msg["result"]["url"].startswith(quote(media.url))
assert msg["result"]["mime_type"] == media.mime_type assert msg["result"]["mime_type"] == media.mime_type
with patch( with patch(

View File

@ -95,5 +95,8 @@ async def test_media_view(hass, hass_client):
resp = await client.get("/media/local/test.mp3") resp = await client.get("/media/local/test.mp3")
assert resp.status == 200 assert resp.status == 200
resp = await client.get("/media/local/Epic Sax Guy 10 Hours.mp4")
assert resp.status == 200
resp = await client.get("/media/recordings/test.mp3") resp = await client.get("/media/recordings/test.mp3")
assert resp.status == 200 assert resp.status == 200

View File

@ -618,7 +618,7 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock)
"percentage_state_topic": "percentage-state-topic1", "percentage_state_topic": "percentage-state-topic1",
"percentage_command_topic": "percentage-command-topic1", "percentage_command_topic": "percentage-command-topic1",
"speed_range_min": 1, "speed_range_min": 1,
"speed_range_max": 100, "speed_range_max": 3,
}, },
{ {
"platform": "mqtt", "platform": "mqtt",
@ -651,9 +651,25 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock)
state = hass.states.get("fan.test1") state = hass.states.get("fan.test1")
assert state.attributes.get(ATTR_ASSUMED_STATE) assert state.attributes.get(ATTR_ASSUMED_STATE)
await common.async_set_percentage(hass, "fan.test1", 33)
mqtt_mock.async_publish.assert_called_once_with(
"percentage-command-topic1", "1", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test1")
assert state.attributes.get(ATTR_ASSUMED_STATE)
await common.async_set_percentage(hass, "fan.test1", 66)
mqtt_mock.async_publish.assert_called_once_with(
"percentage-command-topic1", "2", 0, False
)
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test1")
assert state.attributes.get(ATTR_ASSUMED_STATE)
await common.async_set_percentage(hass, "fan.test1", 100) await common.async_set_percentage(hass, "fan.test1", 100)
mqtt_mock.async_publish.assert_called_once_with( mqtt_mock.async_publish.assert_called_once_with(
"percentage-command-topic1", "100", 0, False "percentage-command-topic1", "3", 0, False
) )
mqtt_mock.async_publish.reset_mock() mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test1") state = hass.states.get("fan.test1")

View File

@ -12,6 +12,7 @@ from homeassistant.components.climate.const import (
ATTR_PRESET_MODE, ATTR_PRESET_MODE,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_LOW,
CURRENT_HVAC_COOL,
CURRENT_HVAC_IDLE, CURRENT_HVAC_IDLE,
DOMAIN as CLIMATE_DOMAIN, DOMAIN as CLIMATE_DOMAIN,
HVAC_MODE_COOL, HVAC_MODE_COOL,
@ -351,6 +352,7 @@ async def test_thermostat_different_endpoints(
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.8 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.8
assert state.attributes[ATTR_FAN_MODE] == "Auto low" assert state.attributes[ATTR_FAN_MODE] == "Auto low"
assert state.attributes[ATTR_FAN_STATE] == "Idle / off" assert state.attributes[ATTR_FAN_STATE] == "Idle / off"
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL
async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integration): async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integration):

View File

@ -528,7 +528,7 @@ async def test_poll_value(
}, },
blocking=True, blocking=True,
) )
assert len(client.async_send_command.call_args_list) == 7 assert len(client.async_send_command.call_args_list) == 8
# Test polling against an invalid entity raises ValueError # Test polling against an invalid entity raises ValueError
with pytest.raises(ValueError): with pytest.raises(ValueError):

View File

@ -0,0 +1 @@
I play the sax