diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index 464283e07f30c..86d85588967e3 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -133,7 +133,7 @@ async def async_step_options(self, user_input=None): ) 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_KNOWN_HOSTS] = known_hosts updated_config[CONF_UUID] = wanted_uuid diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 016d5162d2380..afd6065cb9878 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -7,6 +7,7 @@ import functools as ft import json import logging +from urllib.parse import quote import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -472,7 +473,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): media_id = async_sign_path( self.hass, refresh_token.id, - media_id, + quote(media_id), timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), ) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index fcf83918fda07..4a3deee4d117d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -209,8 +209,11 @@ async def async_step_zeroconf(self, discovery_info): } if "id" not in properties: - _LOGGER.warning( - "HomeKit device %s: id not exposed, in violation of spec", properties + # This can happen if the TXT record is received after the PTR record + # 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") diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 9580a7ee50d19..d4e7eb83ee3bc 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ - "aiohomekit==0.2.60" + "aiohomekit==0.2.61" ], "zeroconf": [ "_hap._tcp.local." diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 3267c9cc70e74..382758194832f 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,6 +1,7 @@ """Authentication for HTTP component.""" import logging import secrets +from urllib.parse import unquote from aiohttp import hdrs from aiohttp.web import middleware @@ -30,11 +31,16 @@ def async_sign_path(hass, refresh_token_id, path, expiration): now = dt_util.utcnow() 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, algorithm="HS256", ) - return f"{path}?{SIGN_QUERY_PARAM}=" f"{encoded.decode()}" + return f"{path}?{SIGN_QUERY_PARAM}={encoded.decode()}" @callback diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index fb2920fb6e25d..c7660f2a3f009 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -114,7 +114,7 @@ def _add_log_filter(logger, patterns): """Add a Filter to the logger based on a regexp of the filter_str.""" 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) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 0e3672f952ed8..e57bfd0c514bd 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from time import gmtime, strftime, time +from time import localtime, strftime, time from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation @@ -82,7 +82,7 @@ vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( cv.time_period, 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())), ) } diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index e38f08809a749..4d610dfc04ffd 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.util.dt import now _LOGGER = logging.getLogger(__name__) @@ -59,7 +60,7 @@ def setup(hass, config): scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds() try: - cube = MaxCube(host, port) + cube = MaxCube(host, port, now=now) hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) except timeout as ex: _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index ddc21bd2358f0..75b5a5fcb6df4 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -2,6 +2,6 @@ "domain": "maxcube", "name": "eQ-3 MAX!", "documentation": "https://www.home-assistant.io/integrations/maxcube", - "requirements": ["maxcube-api==0.4.1"], + "requirements": ["maxcube-api==0.4.2"], "codeowners": [] } diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 0ef5d460580ff..5b027a99bf9ce 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from urllib.parse import quote import voluptuous as vol @@ -123,7 +124,7 @@ async def websocket_resolve_media(hass, connection, msg): url = async_sign_path( hass, connection.refresh_token_id, - url, + quote(url), timedelta(seconds=msg["expires"]), ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 6009b941c5c3d..24c4c805dfda2 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,6 +1,7 @@ """Support for MQTT fans.""" import functools import logging +import math import voluptuous as vol @@ -441,13 +442,12 @@ def speed_received(msg): ) return - if not self._feature_percentage: - if speed in self._legacy_speeds_list_no_off: - self._percentage = ordered_list_item_to_percentage( - self._legacy_speeds_list_no_off, speed - ) - elif speed == SPEED_OFF: - self._percentage = 0 + if speed in self._legacy_speeds_list_no_off: + self._percentage = ordered_list_item_to_percentage( + self._legacy_speeds_list_no_off, speed + ) + elif speed == SPEED_OFF: + self._percentage = 0 self.async_write_ha_state() @@ -592,7 +592,7 @@ async def async_set_percentage(self, percentage: int) -> None: This method is a coroutine. """ - percentage_payload = int( + percentage_payload = math.ceil( percentage_to_ranged_value(self._speed_range, percentage) ) mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index cb3493ebc5598..253400c886d15 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia", - "requirements": ["nexia==0.9.5"], + "requirements": ["nexia==0.9.6"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/util.py b/homeassistant/components/nexia/util.py index 665aa137065cb..74272a3c7fd4f 100644 --- a/homeassistant/components/nexia/util.py +++ b/homeassistant/components/nexia/util.py @@ -13,4 +13,6 @@ def is_invalid_auth_code(http_status_code): def percent_conv(val): """Convert an actual percentage (0.0-1.0) to 0-100 scale.""" + if val is None: + return None return round(val * 100.0, 1) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 7be5efeaf2fd7..b585451cdb076 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -134,8 +134,12 @@ def _update_listeners(): async def _notify_task(self): 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) + elif res is None: + LOGGER.debug("Aborting notify due to unexpected return") + break @callback def _async_notify_stop(self): diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index ad591ad330bdc..36e01d8f3c8a6 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -3,7 +3,7 @@ "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", "requirements": [ - "ha-philipsjs==2.3.2" + "ha-philipsjs==2.7.0" ], "codeowners": [ "@elupus" diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 4a34926545956..f8294c878dde2 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -47,10 +47,12 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" - coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) - await coordinator.async_config_entry_first_refresh() + coordinator = hass.data[DOMAIN].get(entry.entry_id) + if not coordinator: + coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) + 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: hass.async_create_task( diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 11f71cfd33711..c5c082cd509d0 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -195,9 +195,15 @@ def device_info(self): """Return device information for the controller.""" controller_type = self.config_data["controller_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 { "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac)}, "name": self.gateway_name, "manufacturer": "Pentair", - "model": EQUIPMENT.CONTROLLER_HARDWARE[controller_type][hardware_type], + "model": equipment_model, } diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index a9e137968758a..370522415fd78 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -118,15 +118,16 @@ def brightness(self) -> int: """Brightness of light.""" if self.mode == "color": if self.control_result: - brightness = self.control_result["gain"] + brightness_pct = self.control_result["gain"] else: - brightness = self.block.gain + brightness_pct = self.block.gain else: if self.control_result: - brightness = self.control_result["brightness"] + brightness_pct = self.control_result["brightness"] else: - brightness = self.block.brightness - return int(brightness / 100 * 255) + brightness_pct = self.block.brightness + + return round(255 * brightness_pct / 100) @property def white_value(self) -> int: @@ -188,11 +189,11 @@ async def async_turn_on(self, **kwargs) -> None: set_mode = None params = {"turn": "on"} 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"): - params["gain"] = tmp_brightness + params["gain"] = brightness_pct if hasattr(self.block, "brightness"): - params["brightness"] = tmp_brightness + params["brightness"] = brightness_pct if ATTR_COLOR_TEMP in kwargs: color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index c64a5ef788fc9..41ea873c5f892 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -150,6 +150,7 @@ def __init__( THERMOSTAT_OPERATING_STATE_PROPERTY, command_class=CommandClass.THERMOSTAT_OPERATING_STATE, add_to_watched_value_ids=True, + check_all_endpoints=True, ) self._current_temp = self.get_zwave_value( THERMOSTAT_CURRENT_TEMP_PROPERTY, diff --git a/homeassistant/const.py b/homeassistant/const.py index 9e6fe036d5cb5..875e60e220220 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 4 -PATCH_VERSION = "3" +PATCH_VERSION = "4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) diff --git a/requirements_all.txt b/requirements_all.txt index 349847b9791fb..4831b310f8ea8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.60 +aiohomekit==0.2.61 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -721,7 +721,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.3.2 +ha-philipsjs==2.7.0 # homeassistant.components.habitica habitipy==0.2.0 @@ -916,7 +916,7 @@ magicseaweed==1.0.3 matrix-client==0.3.2 # homeassistant.components.maxcube -maxcube-api==0.4.1 +maxcube-api==0.4.2 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 @@ -986,7 +986,7 @@ netdisco==2.8.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.5 +nexia==0.9.6 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8c2c3931d78a..33ef9a95aa82c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -106,7 +106,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.60 +aiohomekit==0.2.61 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -382,7 +382,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.3.2 +ha-philipsjs==2.7.0 # homeassistant.components.habitica habitipy==0.2.0 @@ -476,7 +476,7 @@ logi_circle==0.2.2 luftdaten==0.6.4 # homeassistant.components.maxcube -maxcube-api==0.4.1 +maxcube-api==0.4.2 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 @@ -516,7 +516,7 @@ nessclient==0.9.15 netdisco==2.8.2 # homeassistant.components.nexia -nexia==0.9.5 +nexia==0.9.6 # homeassistant.components.notify_events notify-events==1.0.4 diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py new file mode 100644 index 0000000000000..1febd9d880324 --- /dev/null +++ b/tests/components/cast/test_config_flow.py @@ -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"] + ) diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 888ef2ebcd797..178f721959f87 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -1,39 +1,9 @@ -"""Tests for the Cast config flow.""" -from unittest.mock import ANY, patch +"""Tests for the Cast integration.""" +from unittest.mock import patch -import pytest - -from homeassistant import config_entries, data_entry_flow from homeassistant.components import cast 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): """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): - """Test that no config will not create an entry.""" + """Test that an empty config does not create an entry.""" with patch( "homeassistant.components.cast.async_setup_entry", return_value=True ) as mock_setup: @@ -75,207 +45,3 @@ async def test_not_configuring_cast_not_creates_entry(hass): await hass.async_block_till_done() 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"] - ) diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index d2b0e8931b690..6435ef9539421 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -42,6 +42,7 @@ async def test_log_filtering(hass, caplog): "doesntmatchanything", ".*shouldfilterall.*", "^filterthis:.*", + "in the middle", ], "test.other_filter": [".*otherfilterer"], }, @@ -62,6 +63,7 @@ def msg_test(logger, result, message, *args): 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, False, "this in the middle 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, True, "format string shouldfilter%s", "not") diff --git a/tests/components/maxcube/conftest.py b/tests/components/maxcube/conftest.py index 6b283cf87c025..b36072190c4b7 100644 --- a/tests/components/maxcube/conftest.py +++ b/tests/components/maxcube/conftest.py @@ -10,6 +10,7 @@ from homeassistant.components.maxcube import DOMAIN from homeassistant.setup import async_setup_component +from homeassistant.util.dt import now @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) await hass.async_block_till_done() 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 diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 0dda9f67fbe36..d8ee73ebc2f11 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -1,5 +1,6 @@ """Test Media Source initialization.""" from unittest.mock import patch +from urllib.parse import quote import pytest @@ -45,7 +46,7 @@ async def test_async_browse_media(hass): media = await media_source.async_browse_media(hass, "") assert isinstance(media, media_source.models.BrowseMediaSource) assert media.title == "media/" - assert len(media.children) == 1 + assert len(media.children) == 2 # Test invalid media content with pytest.raises(ValueError): @@ -133,14 +134,15 @@ async def test_websocket_browse_media(hass, hass_ws_client): 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.""" assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() 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( "homeassistant.components.media_source.async_resolve_media", @@ -150,7 +152,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client): { "id": 1, "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["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 with patch( diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index e3e2a3f1617d6..aff4f92be0228 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -95,5 +95,8 @@ async def test_media_view(hass, hass_client): resp = await client.get("/media/local/test.mp3") 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") assert resp.status == 200 diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 5caec9b7473f7..bfa1f387bcd59 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -618,7 +618,7 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock) "percentage_state_topic": "percentage-state-topic1", "percentage_command_topic": "percentage-command-topic1", "speed_range_min": 1, - "speed_range_max": 100, + "speed_range_max": 3, }, { "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") 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) 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() state = hass.states.get("fan.test1") diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 83a607f3add39..2084e771546f6 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -12,6 +12,7 @@ ATTR_PRESET_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, CURRENT_HVAC_IDLE, DOMAIN as CLIMATE_DOMAIN, 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_FAN_MODE] == "Auto low" 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): diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 7bdba7894d2f5..956361d39536a 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -528,7 +528,7 @@ async def test_poll_value( }, 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 with pytest.raises(ValueError): diff --git a/tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 b/tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 new file mode 100644 index 0000000000000..23bd6ccc56486 --- /dev/null +++ b/tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 @@ -0,0 +1 @@ +I play the sax