From 75908f38f3c637f8c67ea03507f0674ca84383a7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 31 Mar 2021 21:32:05 +0200 Subject: [PATCH 001/120] Bumped version to 2021.4.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5dbd9556cd9..9b77b3a4f04 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 = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 16da181692ee8a983c7c9d0fe3aa968761ed4c81 Mon Sep 17 00:00:00 2001 From: youknowjack0 Date: Thu, 1 Apr 2021 09:32:59 -0700 Subject: [PATCH 002/120] Fix timer.finish to cancel callback (#48549) Timer.finish doesn't cancel the callback, which can lead to incorrect early cancellation of the timer if it is subsequently restarted. Bug reported here: https://community.home-assistant.io/t/timer-component-timer-stops-before-time-is-up/96038 --- homeassistant/components/timer/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 216ab3217a5..2ff408dcd81 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -327,7 +327,9 @@ class Timer(RestoreEntity): if self._state != STATUS_ACTIVE: return - self._listener = None + if self._listener: + self._listener() + self._listener = None self._state = STATUS_IDLE self._end = None self._remaining = None From 07827ca55d90161a3aec60ccfa7292929290a062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 1 Apr 2021 01:58:48 +0200 Subject: [PATCH 003/120] Remove analytics from default_config (#48566) --- homeassistant/components/default_config/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index fa7f547869d..0f4b940cc36 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -3,7 +3,6 @@ "name": "Default Config", "documentation": "https://www.home-assistant.io/integrations/default_config", "dependencies": [ - "analytics", "automation", "cloud", "counter", From 3982849275bcb7e16205ef6697dc61b22955adbd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 1 Apr 2021 00:00:39 -0600 Subject: [PATCH 004/120] Fix incorrect constant import in Ambient PWS (#48574) --- homeassistant/components/ambient_station/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 732c28c8dc5..7c60d1da9bc 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -1,6 +1,5 @@ """Support for Ambient Weather Station sensors.""" -from homeassistant.components.binary_sensor import DOMAIN as SENSOR -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity from homeassistant.const import ATTR_NAME from homeassistant.core import callback From f08e7dccdf06235bbd181dfed628f692e4a62d20 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 1 Apr 2021 18:02:28 +0200 Subject: [PATCH 005/120] Don't care about DPI entries when looking for clients to be restored from UniFi (#48579) * DPI switches shouldnt be restored, they're not part of clients to be restored * Only care about Block and POE switch entries --- homeassistant/components/unifi/controller.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index dc56cd9d9e3..c77987bcbdd 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -29,6 +29,7 @@ import async_timeout from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, @@ -347,7 +348,10 @@ class UniFiController: ): if entry.domain == TRACKER_DOMAIN: mac = entry.unique_id.split("-", 1)[0] - elif entry.domain == SWITCH_DOMAIN: + elif entry.domain == SWITCH_DOMAIN and ( + entry.unique_id.startswith(BLOCK_SWITCH) + or entry.unique_id.startswith(POE_SWITCH) + ): mac = entry.unique_id.split("-", 1)[1] else: continue From 5df90b32fc2987cdec1537c7136bbd0004b9e6d5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Apr 2021 15:06:47 +0200 Subject: [PATCH 006/120] Cleanup orphan devices in onewire integration (#48581) * Cleanup orphan devices (https://github.com/home-assistant/core/issues/47438) * Refactor unit testing * Filter device entries for this config entry * Update logging * Cleanup check --- homeassistant/components/onewire/__init__.py | 43 +++- tests/components/onewire/__init__.py | 36 +++ .../{test_entity_owserver.py => const.py} | 216 ++++++++++++------ .../components/onewire/test_binary_sensor.py | 59 ++--- .../components/onewire/test_entity_sysbus.py | 175 -------------- tests/components/onewire/test_init.py | 50 +++- tests/components/onewire/test_sensor.py | 159 +++++++++---- tests/components/onewire/test_switch.py | 90 ++------ 8 files changed, 419 insertions(+), 409 deletions(-) rename tests/components/onewire/{test_entity_owserver.py => const.py} (83%) delete mode 100644 tests/components/onewire/test_entity_sysbus.py diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 779bc6dfd3a..e5a214ce8a4 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -1,13 +1,17 @@ """The 1-Wire component.""" import asyncio +import logging from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, PLATFORMS from .onewirehub import CannotConnect, OneWireHub +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass, config): """Set up 1-Wire integrations.""" @@ -26,10 +30,43 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): hass.data[DOMAIN][config_entry.unique_id] = onewirehub - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) + async def cleanup_registry() -> None: + # Get registries + device_registry, entity_registry = await asyncio.gather( + hass.helpers.device_registry.async_get_registry(), + hass.helpers.entity_registry.async_get_registry(), ) + # Generate list of all device entries + registry_devices = [ + entry.id + for entry in dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + ] + # Remove devices that don't belong to any entity + for device_id in registry_devices: + if not er.async_entries_for_device( + entity_registry, device_id, include_disabled_entities=True + ): + _LOGGER.debug( + "Removing device `%s` because it does not have any entities", + device_id, + ) + device_registry.async_remove_device(device_id) + + async def start_platforms() -> None: + """Start platforms and cleanup devices.""" + # wait until all required platforms are ready + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(config_entry, platform) + for platform in PLATFORMS + ] + ) + await cleanup_registry() + + hass.async_create_task(start_platforms()) + return True diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 716e73747f1..7b85c16d4c8 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from pyownet.protocol import ProtocolError + from homeassistant.components.onewire.const import ( CONF_MOUNT_DIR, CONF_NAMES, @@ -13,6 +15,8 @@ from homeassistant.components.onewire.const import ( from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from .const import MOCK_OWPROXY_DEVICES + from tests.common import MockConfigEntry @@ -89,3 +93,35 @@ async def setup_onewire_patched_owserver_integration(hass): await hass.async_block_till_done() return config_entry + + +def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None: + """Set up mock for owproxy.""" + dir_return_value = [] + main_read_side_effect = [] + sub_read_side_effect = [] + + for device_id in device_ids: + mock_device = MOCK_OWPROXY_DEVICES[device_id] + + # Setup directory listing + dir_return_value += [f"/{device_id}/"] + + # Setup device reads + main_read_side_effect += [device_id[0:2].encode()] + if "inject_reads" in mock_device: + main_read_side_effect += mock_device["inject_reads"] + + # Setup sub-device reads + device_sensors = mock_device.get(domain, []) + for expected_sensor in device_sensors: + sub_read_side_effect.append(expected_sensor["injected_value"]) + + # Ensure enough read side effect + read_side_effect = ( + main_read_side_effect + + sub_read_side_effect + + [ProtocolError("Missing injected value")] * 20 + ) + owproxy.return_value.dir.return_value = dir_return_value + owproxy.return_value.read.side_effect = read_side_effect diff --git a/tests/components/onewire/test_entity_owserver.py b/tests/components/onewire/const.py similarity index 83% rename from tests/components/onewire/test_entity_owserver.py rename to tests/components/onewire/const.py index a3a205795bf..8fa149c7adc 100644 --- a/tests/components/onewire/test_entity_owserver.py +++ b/tests/components/onewire/const.py @@ -1,11 +1,10 @@ -"""Tests for 1-Wire devices connected on OWServer.""" -from unittest.mock import patch +"""Constants for 1-Wire integration.""" +from pi1wire import InvalidCRCException, UnsupportResponseException from pyownet.protocol import Error as ProtocolError -import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.onewire.const import DOMAIN, PLATFORMS, PRESSURE_CBAR +from homeassistant.components.onewire.const import DOMAIN, PRESSURE_CBAR from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -24,13 +23,8 @@ from homeassistant.const import ( TEMP_CELSIUS, VOLT, ) -from homeassistant.setup import async_setup_component -from . import setup_onewire_patched_owserver_integration - -from tests.common import mock_device_registry, mock_registry - -MOCK_DEVICE_SENSORS = { +MOCK_OWPROXY_DEVICES = { "00.111111111111": { "inject_reads": [ b"", # read device type @@ -186,7 +180,42 @@ MOCK_DEVICE_SENSORS = { "model": "DS2409", "name": "1F.111111111111", }, - SENSOR_DOMAIN: [], + "branches": { + "aux": {}, + "main": { + "1D.111111111111": { + "inject_reads": [ + b"DS2423", # read device type + ], + "device_info": { + "identifiers": {(DOMAIN, "1D.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "DS2423", + "name": "1D.111111111111", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.1d_111111111111_counter_a", + "device_file": "/1F.111111111111/main/1D.111111111111/counter.A", + "unique_id": "/1D.111111111111/counter.A", + "injected_value": b" 251123", + "result": "251123", + "unit": "count", + "class": None, + }, + { + "entity_id": "sensor.1d_111111111111_counter_b", + "device_file": "/1F.111111111111/main/1D.111111111111/counter.B", + "unique_id": "/1D.111111111111/counter.B", + "injected_value": b" 248125", + "result": "248125", + "unit": "count", + "class": None, + }, + ], + }, + }, + }, }, "22.111111111111": { "inject_reads": [ @@ -748,65 +777,106 @@ MOCK_DEVICE_SENSORS = { }, } - -@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys()) -@pytest.mark.parametrize("platform", PLATFORMS) -@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") -async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): - """Test for 1-Wire device. - - As they would be on a clean setup: all binary-sensors and switches disabled. - """ - await async_setup_component(hass, "persistent_notification", {}) - entity_registry = mock_registry(hass) - device_registry = mock_device_registry(hass) - - mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] - - device_family = device_id[0:2] - dir_return_value = [f"/{device_id}/"] - read_side_effect = [device_family.encode()] - if "inject_reads" in mock_device_sensor: - read_side_effect += mock_device_sensor["inject_reads"] - - expected_sensors = mock_device_sensor.get(platform, []) - for expected_sensor in expected_sensors: - read_side_effect.append(expected_sensor["injected_value"]) - - # Ensure enough read side effect - read_side_effect.extend([ProtocolError("Missing injected value")] * 20) - owproxy.return_value.dir.return_value = dir_return_value - owproxy.return_value.read.side_effect = read_side_effect - - with patch("homeassistant.components.onewire.PLATFORMS", [platform]): - await setup_onewire_patched_owserver_integration(hass) - await hass.async_block_till_done() - - assert len(entity_registry.entities) == len(expected_sensors) - - if len(expected_sensors) > 0: - device_info = mock_device_sensor["device_info"] - assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) - assert registry_entry is not None - assert registry_entry.identifiers == {(DOMAIN, device_id)} - assert registry_entry.manufacturer == device_info["manufacturer"] - assert registry_entry.name == device_info["name"] - assert registry_entry.model == device_info["model"] - - for expected_sensor in expected_sensors: - entity_id = expected_sensor["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_sensor["unique_id"] - assert registry_entry.unit_of_measurement == expected_sensor["unit"] - assert registry_entry.device_class == expected_sensor["class"] - assert registry_entry.disabled == expected_sensor.get("disabled", False) - state = hass.states.get(entity_id) - if registry_entry.disabled: - assert state is None - else: - assert state.state == expected_sensor["result"] - assert state.attributes["device_file"] == expected_sensor.get( - "device_file", registry_entry.unique_id - ) +MOCK_SYSBUS_DEVICES = { + "00-111111111111": {"sensors": []}, + "10-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "10-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "10", + "name": "10-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.my_ds18b20_temperature", + "unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave", + "injected_value": 25.123, + "result": "25.1", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "12-111111111111": {"sensors": []}, + "1D-111111111111": {"sensors": []}, + "22-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "22-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "22", + "name": "22-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.22_111111111111_temperature", + "unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave", + "injected_value": FileNotFoundError, + "result": "unknown", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "26-111111111111": {"sensors": []}, + "28-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "28-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "28", + "name": "28-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.28_111111111111_temperature", + "unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave", + "injected_value": InvalidCRCException, + "result": "unknown", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "29-111111111111": {"sensors": []}, + "3B-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "3B-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "3B", + "name": "3B-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.3b_111111111111_temperature", + "unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave", + "injected_value": 29.993, + "result": "30.0", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "42-111111111111": { + "device_info": { + "identifiers": {(DOMAIN, "42-111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "42", + "name": "42-111111111111", + }, + "sensors": [ + { + "entity_id": "sensor.42_111111111111_temperature", + "unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave", + "injected_value": UnsupportResponseException, + "result": "unknown", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + ], + }, + "EF-111111111111": { + "sensors": [], + }, + "EF-111111111112": { + "sensors": [], + }, +} diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index dd44510e0ad..91ae472278a 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -2,40 +2,25 @@ import copy from unittest.mock import patch -from pyownet.protocol import Error as ProtocolError import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.onewire.binary_sensor import DEVICE_BINARY_SENSORS -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from . import setup_onewire_patched_owserver_integration +from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES from tests.common import mock_registry -MOCK_DEVICE_SENSORS = { - "12.111111111111": { - "inject_reads": [ - b"DS2406", # read device type - ], - BINARY_SENSOR_DOMAIN: [ - { - "entity_id": "binary_sensor.12_111111111111_sensed_a", - "injected_value": b" 1", - "result": STATE_ON, - }, - { - "entity_id": "binary_sensor.12_111111111111_sensed_b", - "injected_value": b" 0", - "result": STATE_OFF, - }, - ], - }, +MOCK_BINARY_SENSORS = { + key: value + for (key, value) in MOCK_OWPROXY_DEVICES.items() + if BINARY_SENSOR_DOMAIN in value } -@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys()) +@pytest.mark.parametrize("device_id", MOCK_BINARY_SENSORS.keys()) @patch("homeassistant.components.onewire.onewirehub.protocol.proxy") async def test_owserver_binary_sensor(owproxy, hass, device_id): """Test for 1-Wire binary sensor. @@ -45,26 +30,14 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id): await async_setup_component(hass, "persistent_notification", {}) entity_registry = mock_registry(hass) - mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] + setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) - device_family = device_id[0:2] - dir_return_value = [f"/{device_id}/"] - read_side_effect = [device_family.encode()] - if "inject_reads" in mock_device_sensor: - read_side_effect += mock_device_sensor["inject_reads"] - - expected_sensors = mock_device_sensor[BINARY_SENSOR_DOMAIN] - for expected_sensor in expected_sensors: - read_side_effect.append(expected_sensor["injected_value"]) - - # Ensure enough read side effect - read_side_effect.extend([ProtocolError("Missing injected value")] * 10) - owproxy.return_value.dir.return_value = dir_return_value - owproxy.return_value.read.side_effect = read_side_effect + mock_device = MOCK_BINARY_SENSORS[device_id] + expected_entities = mock_device[BINARY_SENSOR_DOMAIN] # Force enable binary sensors patch_device_binary_sensors = copy.deepcopy(DEVICE_BINARY_SENSORS) - for item in patch_device_binary_sensors[device_family]: + for item in patch_device_binary_sensors[device_id[0:2]]: item["default_disabled"] = False with patch( @@ -76,14 +49,14 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id): await setup_onewire_patched_owserver_integration(hass) await hass.async_block_till_done() - assert len(entity_registry.entities) == len(expected_sensors) + assert len(entity_registry.entities) == len(expected_entities) - for expected_sensor in expected_sensors: - entity_id = expected_sensor["entity_id"] + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] - assert state.attributes["device_file"] == expected_sensor.get( + assert state.state == expected_entity["result"] + assert state.attributes["device_file"] == expected_entity.get( "device_file", registry_entry.unique_id ) diff --git a/tests/components/onewire/test_entity_sysbus.py b/tests/components/onewire/test_entity_sysbus.py deleted file mode 100644 index 61a38c10f73..00000000000 --- a/tests/components/onewire/test_entity_sysbus.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Tests for 1-Wire devices connected on SysBus.""" -from unittest.mock import patch - -from pi1wire import InvalidCRCException, UnsupportResponseException -import pytest - -from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS -from homeassistant.setup import async_setup_component - -from tests.common import mock_device_registry, mock_registry - -MOCK_CONFIG = { - SENSOR_DOMAIN: { - "platform": DOMAIN, - "mount_dir": DEFAULT_SYSBUS_MOUNT_DIR, - "names": { - "10-111111111111": "My DS18B20", - }, - } -} - -MOCK_DEVICE_SENSORS = { - "00-111111111111": {"sensors": []}, - "10-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "10-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "10", - "name": "10-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.my_ds18b20_temperature", - "unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave", - "injected_value": 25.123, - "result": "25.1", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "12-111111111111": {"sensors": []}, - "1D-111111111111": {"sensors": []}, - "22-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "22-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "22", - "name": "22-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.22_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave", - "injected_value": FileNotFoundError, - "result": "unknown", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "26-111111111111": {"sensors": []}, - "28-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "28-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "28", - "name": "28-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.28_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave", - "injected_value": InvalidCRCException, - "result": "unknown", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "29-111111111111": {"sensors": []}, - "3B-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "3B-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "3B", - "name": "3B-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.3b_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave", - "injected_value": 29.993, - "result": "30.0", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "42-111111111111": { - "device_info": { - "identifiers": {(DOMAIN, "42-111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "42", - "name": "42-111111111111", - }, - "sensors": [ - { - "entity_id": "sensor.42_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave", - "injected_value": UnsupportResponseException, - "result": "unknown", - "unit": TEMP_CELSIUS, - "class": DEVICE_CLASS_TEMPERATURE, - }, - ], - }, - "EF-111111111111": { - "sensors": [], - }, - "EF-111111111112": { - "sensors": [], - }, -} - - -@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys()) -async def test_onewiredirect_setup_valid_device(hass, device_id): - """Test that sysbus config entry works correctly.""" - entity_registry = mock_registry(hass) - device_registry = mock_device_registry(hass) - - mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] - - glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"] - read_side_effect = [] - expected_sensors = mock_device_sensor["sensors"] - for expected_sensor in expected_sensors: - read_side_effect.append(expected_sensor["injected_value"]) - - # Ensure enough read side effect - read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20) - - with patch( - "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True - ), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch( - "pi1wire.OneWire.get_temperature", - side_effect=read_side_effect, - ): - assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_CONFIG) - await hass.async_block_till_done() - - assert len(entity_registry.entities) == len(expected_sensors) - - if len(expected_sensors) > 0: - device_info = mock_device_sensor["device_info"] - assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) - assert registry_entry is not None - assert registry_entry.identifiers == {(DOMAIN, device_id)} - assert registry_entry.manufacturer == device_info["manufacturer"] - assert registry_entry.name == device_info["name"] - assert registry_entry.model == device_info["model"] - - for expected_sensor in expected_sensors: - entity_id = expected_sensor["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_sensor["unique_id"] - assert registry_entry.unit_of_measurement == expected_sensor["unit"] - assert registry_entry.device_class == expected_sensor["class"] - state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 38e97206698..5783b241a2f 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import patch from pyownet.protocol import ConnError, OwnetError from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ( CONN_CLASS_LOCAL_POLL, ENTRY_STATE_LOADED, @@ -11,10 +12,17 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component -from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration +from . import ( + setup_onewire_owserver_integration, + setup_onewire_patched_owserver_integration, + setup_onewire_sysbus_integration, + setup_owproxy_mock_devices, +) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_device_registry, mock_registry async def test_owserver_connect_failure(hass): @@ -87,3 +95,41 @@ async def test_unload_entry(hass): assert config_entry_owserver.state == ENTRY_STATE_NOT_LOADED assert config_entry_sysbus.state == ENTRY_STATE_NOT_LOADED assert not hass.data.get(DOMAIN) + + +@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") +async def test_registry_cleanup(owproxy, hass): + """Test for 1-Wire device. + + As they would be on a clean setup: all binary-sensors and switches disabled. + """ + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + # Initialise with two components + setup_owproxy_mock_devices( + owproxy, SENSOR_DOMAIN, ["10.111111111111", "28.111111111111"] + ) + with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): + await setup_onewire_patched_owserver_integration(hass) + await hass.async_block_till_done() + + assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 + assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 2 + + # Second item has disappeared from bus, and was removed manually from the front-end + setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, ["10.111111111111"]) + entity_registry.async_remove("sensor.28_111111111111_temperature") + await hass.async_block_till_done() + + assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 + assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 + + # Second item has disappeared from bus, and was removed manually from the front-end + with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): + await hass.config_entries.async_reload("2") + await hass.async_block_till_done() + + assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 + assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 1 diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 44351cf9a63..f81044eb86d 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -4,54 +4,29 @@ from unittest.mock import patch from pyownet.protocol import Error as ProtocolError import pytest -from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN +from homeassistant.components.onewire.const import ( + DEFAULT_SYSBUS_MOUNT_DIR, + DOMAIN, + PLATFORMS, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.setup import async_setup_component -from . import setup_onewire_patched_owserver_integration +from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES -from tests.common import assert_setup_component, mock_registry +from tests.common import assert_setup_component, mock_device_registry, mock_registry MOCK_COUPLERS = { - "1F.111111111111": { - "inject_reads": [ - b"DS2409", # read device type - ], - "branches": { - "aux": {}, - "main": { - "1D.111111111111": { - "inject_reads": [ - b"DS2423", # read device type - ], - "device_info": { - "identifiers": {(DOMAIN, "1D.111111111111")}, - "manufacturer": "Maxim Integrated", - "model": "DS2423", - "name": "1D.111111111111", - }, - SENSOR_DOMAIN: [ - { - "entity_id": "sensor.1d_111111111111_counter_a", - "device_file": "/1F.111111111111/main/1D.111111111111/counter.A", - "unique_id": "/1D.111111111111/counter.A", - "injected_value": b" 251123", - "result": "251123", - "unit": "count", - "class": None, - }, - { - "entity_id": "sensor.1d_111111111111_counter_b", - "device_file": "/1F.111111111111/main/1D.111111111111/counter.B", - "unique_id": "/1D.111111111111/counter.B", - "injected_value": b" 248125", - "result": "248125", - "unit": "count", - "class": None, - }, - ], - }, - }, + key: value for (key, value) in MOCK_OWPROXY_DEVICES.items() if "branches" in value +} + +MOCK_SYSBUS_CONFIG = { + SENSOR_DOMAIN: { + "platform": DOMAIN, + "mount_dir": DEFAULT_SYSBUS_MOUNT_DIR, + "names": { + "10-111111111111": "My DS18B20", }, } } @@ -154,3 +129,103 @@ async def test_sensors_on_owserver_coupler(owproxy, hass, device_id): else: assert state.state == expected_sensor["result"] assert state.attributes["device_file"] == expected_sensor["device_file"] + + +@pytest.mark.parametrize("device_id", MOCK_OWPROXY_DEVICES.keys()) +@pytest.mark.parametrize("platform", PLATFORMS) +@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") +async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): + """Test for 1-Wire device. + + As they would be on a clean setup: all binary-sensors and switches disabled. + """ + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + setup_owproxy_mock_devices(owproxy, platform, [device_id]) + + mock_device = MOCK_OWPROXY_DEVICES[device_id] + expected_entities = mock_device.get(platform, []) + + with patch("homeassistant.components.onewire.PLATFORMS", [platform]): + await setup_onewire_patched_owserver_integration(hass) + await hass.async_block_till_done() + + assert len(entity_registry.entities) == len(expected_entities) + + if len(expected_entities) > 0: + device_info = mock_device["device_info"] + assert len(device_registry.devices) == 1 + registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) + assert registry_entry is not None + assert registry_entry.identifiers == {(DOMAIN, device_id)} + assert registry_entry.manufacturer == device_info["manufacturer"] + assert registry_entry.name == device_info["name"] + assert registry_entry.model == device_info["model"] + + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unit_of_measurement == expected_entity["unit"] + assert registry_entry.device_class == expected_entity["class"] + assert registry_entry.disabled == expected_entity.get("disabled", False) + state = hass.states.get(entity_id) + if registry_entry.disabled: + assert state is None + else: + assert state.state == expected_entity["result"] + assert state.attributes["device_file"] == expected_entity.get( + "device_file", registry_entry.unique_id + ) + + +@pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys()) +async def test_onewiredirect_setup_valid_device(hass, device_id): + """Test that sysbus config entry works correctly.""" + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + mock_device_sensor = MOCK_SYSBUS_DEVICES[device_id] + + glob_result = [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"] + read_side_effect = [] + expected_sensors = mock_device_sensor["sensors"] + for expected_sensor in expected_sensors: + read_side_effect.append(expected_sensor["injected_value"]) + + # Ensure enough read side effect + read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20) + + with patch( + "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True + ), patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch( + "pi1wire.OneWire.get_temperature", + side_effect=read_side_effect, + ): + assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_SYSBUS_CONFIG) + await hass.async_block_till_done() + + assert len(entity_registry.entities) == len(expected_sensors) + + if len(expected_sensors) > 0: + device_info = mock_device_sensor["device_info"] + assert len(device_registry.devices) == 1 + registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) + assert registry_entry is not None + assert registry_entry.identifiers == {(DOMAIN, device_id)} + assert registry_entry.manufacturer == device_info["manufacturer"] + assert registry_entry.name == device_info["name"] + assert registry_entry.model == device_info["model"] + + for expected_sensor in expected_sensors: + entity_id = expected_sensor["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_sensor["unique_id"] + assert registry_entry.unit_of_measurement == expected_sensor["unit"] + assert registry_entry.device_class == expected_sensor["class"] + state = hass.states.get(entity_id) + assert state.state == expected_sensor["result"] diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 0d8c9918711..91a9e32e902 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -2,7 +2,6 @@ import copy from unittest.mock import patch -from pyownet.protocol import Error as ProtocolError import pytest from homeassistant.components.onewire.switch import DEVICE_SWITCHES @@ -10,58 +9,19 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TOGGLE, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from . import setup_onewire_patched_owserver_integration +from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES from tests.common import mock_registry -MOCK_DEVICE_SENSORS = { - "12.111111111111": { - "inject_reads": [ - b"DS2406", # read device type - ], - SWITCH_DOMAIN: [ - { - "entity_id": "switch.12_111111111111_pio_a", - "unique_id": "/12.111111111111/PIO.A", - "injected_value": b" 1", - "result": STATE_ON, - "unit": None, - "class": None, - "disabled": True, - }, - { - "entity_id": "switch.12_111111111111_pio_b", - "unique_id": "/12.111111111111/PIO.B", - "injected_value": b" 0", - "result": STATE_OFF, - "unit": None, - "class": None, - "disabled": True, - }, - { - "entity_id": "switch.12_111111111111_latch_a", - "unique_id": "/12.111111111111/latch.A", - "injected_value": b" 1", - "result": STATE_ON, - "unit": None, - "class": None, - "disabled": True, - }, - { - "entity_id": "switch.12_111111111111_latch_b", - "unique_id": "/12.111111111111/latch.B", - "injected_value": b" 0", - "result": STATE_OFF, - "unit": None, - "class": None, - "disabled": True, - }, - ], - } +MOCK_SWITCHES = { + key: value + for (key, value) in MOCK_OWPROXY_DEVICES.items() + if SWITCH_DOMAIN in value } -@pytest.mark.parametrize("device_id", ["12.111111111111"]) +@pytest.mark.parametrize("device_id", MOCK_SWITCHES.keys()) @patch("homeassistant.components.onewire.onewirehub.protocol.proxy") async def test_owserver_switch(owproxy, hass, device_id): """Test for 1-Wire switch. @@ -71,26 +31,14 @@ async def test_owserver_switch(owproxy, hass, device_id): await async_setup_component(hass, "persistent_notification", {}) entity_registry = mock_registry(hass) - mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] + setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) - device_family = device_id[0:2] - dir_return_value = [f"/{device_id}/"] - read_side_effect = [device_family.encode()] - if "inject_reads" in mock_device_sensor: - read_side_effect += mock_device_sensor["inject_reads"] - - expected_sensors = mock_device_sensor[SWITCH_DOMAIN] - for expected_sensor in expected_sensors: - read_side_effect.append(expected_sensor["injected_value"]) - - # Ensure enough read side effect - read_side_effect.extend([ProtocolError("Missing injected value")] * 10) - owproxy.return_value.dir.return_value = dir_return_value - owproxy.return_value.read.side_effect = read_side_effect + mock_device = MOCK_SWITCHES[device_id] + expected_entities = mock_device[SWITCH_DOMAIN] # Force enable switches patch_device_switches = copy.deepcopy(DEVICE_SWITCHES) - for item in patch_device_switches[device_family]: + for item in patch_device_switches[device_id[0:2]]: item["default_disabled"] = False with patch( @@ -101,21 +49,21 @@ async def test_owserver_switch(owproxy, hass, device_id): await setup_onewire_patched_owserver_integration(hass) await hass.async_block_till_done() - assert len(entity_registry.entities) == len(expected_sensors) + assert len(entity_registry.entities) == len(expected_entities) - for expected_sensor in expected_sensors: - entity_id = expected_sensor["entity_id"] + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] + assert state.state == expected_entity["result"] if state.state == STATE_ON: owproxy.return_value.read.side_effect = [b" 0"] - expected_sensor["result"] = STATE_OFF + expected_entity["result"] = STATE_OFF elif state.state == STATE_OFF: owproxy.return_value.read.side_effect = [b" 1"] - expected_sensor["result"] = STATE_ON + expected_entity["result"] = STATE_ON await hass.services.async_call( SWITCH_DOMAIN, @@ -126,7 +74,7 @@ async def test_owserver_switch(owproxy, hass, device_id): await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] - assert state.attributes["device_file"] == expected_sensor.get( + assert state.state == expected_entity["result"] + assert state.attributes["device_file"] == expected_entity.get( "device_file", registry_entry.unique_id ) From 947ac514b9ea09699436b809791deccd741254ce Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Apr 2021 16:20:53 +0200 Subject: [PATCH 007/120] Return config entry details for 1-step config flows (#48585) --- .../components/config/config_entries.py | 27 +++++++++---------- .../components/config/test_config_entries.py | 12 ++++++++- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index af90cdcba4b..edf94268741 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -97,6 +97,17 @@ class ConfigManagerEntryResourceReloadView(HomeAssistantView): return self.json({"require_restart": not result}) +def _prepare_config_flow_result_json(result, prepare_result_json): + """Convert result to JSON.""" + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return prepare_result_json(result) + + data = result.copy() + data["result"] = entry_json(result["result"]) + data.pop("data") + return data + + class ConfigManagerFlowIndexView(FlowManagerIndexView): """View to create config flows.""" @@ -118,13 +129,7 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): def _prepare_result_json(self, result): """Convert result to JSON.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return super()._prepare_result_json(result) - - data = result.copy() - data["result"] = data["result"].entry_id - data.pop("data") - return data + return _prepare_config_flow_result_json(result, super()._prepare_result_json) class ConfigManagerFlowResourceView(FlowManagerResourceView): @@ -151,13 +156,7 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView): def _prepare_result_json(self, result): """Convert result to JSON.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return super()._prepare_result_json(result) - - data = result.copy() - data["result"] = entry_json(result["result"]) - data.pop("data") - return data + return _prepare_config_flow_result_json(result, super()._prepare_result_json) class ConfigManagerAvailableFlowView(HomeAssistantView): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 7e4df556fa5..128d0798b66 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -320,7 +320,17 @@ async def test_create_account(hass, client): "title": "Test Entry", "type": "create_entry", "version": 1, - "result": entries[0].entry_id, + "result": { + "connection_class": "unknown", + "disabled_by": None, + "domain": "test", + "entry_id": entries[0].entry_id, + "source": "user", + "state": "loaded", + "supports_options": False, + "supports_unload": False, + "title": "Test Entry", + }, "description": None, "description_placeholders": None, } From f0bd3c577ffd74c30dace67c825ea0927ccf30a9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Apr 2021 22:32:49 +0200 Subject: [PATCH 008/120] Upgrade numpy to 1.20.2 (#48597) --- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 6445b4ad91f..145972e2875 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,6 +3,6 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.19.2", "pyiqvia==0.3.1"], + "requirements": ["numpy==1.20.2", "pyiqvia==0.3.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 24b84e305e7..a0294a7aa49 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,6 +2,6 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.19.2", "opencv-python-headless==4.3.0.36"], + "requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"], "codeowners": [] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 49ee22176a7..84619680490 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.3.0", "tf-models-official==2.3.0", "pycocotools==2.0.1", - "numpy==1.19.2", + "numpy==1.20.2", "pillow==8.1.2" ], "codeowners": [] diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 88e32ce4a46..2bb3719fe95 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.19.2"], + "requirements": ["numpy==1.20.2"], "codeowners": [], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index f7c309bfa29..4a112c35362 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1019,7 +1019,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.19.2 +numpy==1.20.2 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71a5a9213f9..930303b09c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -531,7 +531,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.19.2 +numpy==1.20.2 # homeassistant.components.google oauth2client==4.0.0 From c2d17a72b790fb24c3eb857df39c1cd71b65379d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Apr 2021 22:10:01 +0200 Subject: [PATCH 009/120] Allow templatable service target to support scripts (#48600) --- homeassistant/helpers/config_validation.py | 2 +- homeassistant/helpers/service.py | 11 +++++++--- tests/helpers/test_service.py | 24 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7f2f1550cfe..9b56bb06865 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -916,7 +916,7 @@ SERVICE_SCHEMA = vol.All( vol.Optional("data"): vol.All(dict, template_complex), vol.Optional("data_template"): vol.All(dict, template_complex), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, - vol.Optional(CONF_TARGET): ENTITY_SERVICE_FIELDS, + vol.Optional(CONF_TARGET): vol.Any(ENTITY_SERVICE_FIELDS, dynamic_template), } ), has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 01992d43221..4e484c6aaab 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -204,10 +204,15 @@ def async_prepare_call_from_config( target = {} if CONF_TARGET in config: - conf = config.get(CONF_TARGET) + conf = config[CONF_TARGET] try: - template.attach(hass, conf) - target.update(template.render_complex(conf, variables)) + if isinstance(conf, template.Template): + conf.hass = hass + target.update(conf.async_render(variables)) + else: + template.attach(hass, conf) + target.update(template.render_complex(conf, variables)) + if CONF_ENTITY_ID in target: target[CONF_ENTITY_ID] = cv.comp_entity_ids(target[CONF_ENTITY_ID]) except TemplateError as ex: diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index d168c8b9cfc..7538c0f6f2c 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -213,6 +213,30 @@ class TestServiceHelpers(unittest.TestCase): "entity_id": ["light.static", "light.dynamic"], } + config = { + "service": "{{ 'test_domain.test_service' }}", + "target": "{{ var_target }}", + } + + service.call_from_config( + self.hass, + config, + variables={ + "var_target": { + "entity_id": "light.static", + "area_id": ["area-42", "area-51"], + }, + }, + ) + + service.call_from_config(self.hass, config) + self.hass.block_till_done() + + assert dict(self.calls[2].data) == { + "area_id": ["area-42", "area-51"], + "entity_id": ["light.static"], + } + def test_service_template_service_call(self): """Test legacy service_template call with templating.""" config = { From f0f8b79be0985d7b61cf2e0ab7fac5a10e7107e9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Apr 2021 00:31:19 +0200 Subject: [PATCH 010/120] Fix websocket search for related (#48603) Co-authored-by: Paulus Schoutsen --- homeassistant/components/search/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index 291ef0b52e2..3198f40720b 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -19,7 +19,6 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "search/related", @@ -38,6 +37,7 @@ async def async_setup(hass: HomeAssistant, config: dict): vol.Required("item_id"): str, } ) +@callback def websocket_search_related(hass, connection, msg): """Handle search.""" searcher = Searcher( From d1a48c7c5c5325ac94b218988b6a063d86abf1af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Apr 2021 16:22:08 -0700 Subject: [PATCH 011/120] Clean up mobile app (#48607) Co-authored-by: Martin Hjelmare --- .../components/mobile_app/binary_sensor.py | 6 ++-- homeassistant/components/mobile_app/entity.py | 12 +++----- homeassistant/components/mobile_app/notify.py | 11 +++---- homeassistant/components/mobile_app/sensor.py | 6 ++-- .../components/mobile_app/webhook.py | 8 ++--- homeassistant/util/logging.py | 6 +++- tests/util/test_logging.py | 29 +++++++++++++++++++ 7 files changed, 51 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 36897dd9f69..616cd97a775 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -1,6 +1,4 @@ """Binary sensor platform for mobile_app.""" -from functools import partial - from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON from homeassistant.core import callback @@ -48,7 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) @callback - def handle_sensor_registration(webhook_id, data): + def handle_sensor_registration(data): if data[CONF_WEBHOOK_ID] != webhook_id: return @@ -66,7 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect( hass, f"{DOMAIN}_{ENTITY_TYPE}_register", - partial(handle_sensor_registration, webhook_id), + handle_sensor_registration, ) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 2f30c4b9f1b..46f4589fa2c 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -34,13 +34,14 @@ class MobileAppEntity(RestoreEntity): self._registration = entry.data self._unique_id = config[CONF_UNIQUE_ID] self._entity_type = config[ATTR_SENSOR_TYPE] - self.unsub_dispatcher = None self._name = config[CONF_NAME] async def async_added_to_hass(self): """Register callbacks.""" - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update + ) ) state = await self.async_get_last_state() @@ -49,11 +50,6 @@ class MobileAppEntity(RestoreEntity): self.async_restore_last_state(state) - async def async_will_remove_from_hass(self): - """Disconnect dispatcher listener when removed.""" - if self.unsub_dispatcher is not None: - self.unsub_dispatcher() - @callback def async_restore_last_state(self, last_state): """Restore previous state.""" diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 763186df998..803f00764e7 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -84,17 +84,16 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO): async def async_get_service(hass, config, discovery_info=None): """Get the mobile_app notification service.""" - session = async_get_clientsession(hass) - service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(session) + service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(hass) return service class MobileAppNotificationService(BaseNotificationService): """Implement the notification service for mobile_app.""" - def __init__(self, session): + def __init__(self, hass): """Initialize the service.""" - self._session = session + self._hass = hass @property def targets(self): @@ -141,7 +140,9 @@ class MobileAppNotificationService(BaseNotificationService): try: with async_timeout.timeout(10): - response = await self._session.post(push_url, json=data) + response = await async_get_clientsession(self._hass).post( + push_url, json=data + ) result = await response.json() if response.status in [HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED]: diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 3f4c7d56f3f..7e3c1c13148 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,6 +1,4 @@ """Sensor platform for mobile_app.""" -from functools import partial - from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID from homeassistant.core import callback @@ -50,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) @callback - def handle_sensor_registration(webhook_id, data): + def handle_sensor_registration(data): if data[CONF_WEBHOOK_ID] != webhook_id: return @@ -68,7 +66,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect( hass, f"{DOMAIN}_{ENTITY_TYPE}_register", - partial(handle_sensor_registration, webhook_id), + handle_sensor_registration, ) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index efef6eb1c8a..6be39f34f00 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -472,6 +472,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): device_name = config_entry.data[ATTR_DEVICE_NAME] resp = {} + for sensor in data: entity_type = sensor[ATTR_SENSOR_TYPE] @@ -495,8 +496,6 @@ async def webhook_update_sensor_states(hass, config_entry, data): } continue - entry = {CONF_WEBHOOK_ID: config_entry.data[CONF_WEBHOOK_ID]} - try: sensor = sensor_schema_full(sensor) except vol.Invalid as err: @@ -513,9 +512,8 @@ async def webhook_update_sensor_states(hass, config_entry, data): } continue - new_state = {**entry, **sensor} - - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) + sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, sensor) resp[unique_id] = {"success": True} diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 5653523b677..ba846c0e8b4 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -11,7 +11,7 @@ import traceback from typing import Any, Awaitable, Callable, Coroutine, cast, overload from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback, is_callback class HideSensitiveDataFilter(logging.Filter): @@ -138,6 +138,7 @@ def catch_log_exception( log_exception(format_err, *args) wrapper_func = async_wrapper + else: @wraps(func) @@ -148,6 +149,9 @@ def catch_log_exception( except Exception: # pylint: disable=broad-except log_exception(format_err, *args) + if is_callback(check_func): + wrapper = callback(wrapper) + wrapper_func = wrapper return wrapper_func diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 1a82c35e82d..9277d92f368 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -1,11 +1,13 @@ """Test Home Assistant logging util methods.""" import asyncio +from functools import partial import logging import queue from unittest.mock import patch import pytest +from homeassistant.core import callback, is_callback import homeassistant.util.logging as logging_util @@ -80,3 +82,30 @@ async def test_async_create_catching_coro(hass, caplog): await hass.async_block_till_done() assert "This is a bad coroutine" in caplog.text assert "in test_async_create_catching_coro" in caplog.text + + +def test_catch_log_exception(): + """Test it is still a callback after wrapping including partial.""" + + async def async_meth(): + pass + + assert asyncio.iscoroutinefunction( + logging_util.catch_log_exception(partial(async_meth), lambda: None) + ) + + @callback + def callback_meth(): + pass + + assert is_callback( + logging_util.catch_log_exception(partial(callback_meth), lambda: None) + ) + + def sync_meth(): + pass + + wrapped = logging_util.catch_log_exception(partial(sync_meth), lambda: None) + + assert not is_callback(wrapped) + assert not asyncio.iscoroutinefunction(wrapped) From 8d0941ba6594f0ab80a01296a4b02295688c8d25 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Apr 2021 01:19:57 +0200 Subject: [PATCH 012/120] Update frontend to 20210402.0 (#48609) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 234996afe4f..60ea0ff53b2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210331.0" + "home-assistant-frontend==20210402.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index edb1b2c9cc7..a2a9e5eac2c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210331.0 +home-assistant-frontend==20210402.0 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 4a112c35362..09a59e97390 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210331.0 +home-assistant-frontend==20210402.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 930303b09c7..59708a9fd77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210331.0 +home-assistant-frontend==20210402.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 68b189cf9f348344d03a1d7d6059f2c0ad7870c0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 2 Apr 2021 01:22:36 +0200 Subject: [PATCH 013/120] Increase time out for http requests done in Axis integration (#48610) --- homeassistant/components/axis/device.py | 2 +- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 8c2a43c44ed..f732ad2fb5d 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -304,7 +304,7 @@ async def get_device(hass, host, port, username, password): ) try: - with async_timeout.timeout(15): + with async_timeout.timeout(30): await device.vapix.initialize() return device diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index a78d916da9e..b709ac35da2 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==43"], + "requirements": ["axis==44"], "dhcp": [ { "hostname": "axis-00408c*", "macaddress": "00408C*" }, { "hostname": "axis-accc8e*", "macaddress": "ACCC8E*" }, diff --git a/requirements_all.txt b/requirements_all.txt index 09a59e97390..278ae107af6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -313,7 +313,7 @@ av==8.0.3 # avion==0.10 # homeassistant.components.axis -axis==43 +axis==44 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59708a9fd77..e8fdcceedf8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -187,7 +187,7 @@ auroranoaa==0.0.2 av==8.0.3 # homeassistant.components.axis -axis==43 +axis==44 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 From 39f68de5fa4711520dec582163197a4f32dfddbf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Apr 2021 23:23:47 +0000 Subject: [PATCH 014/120] Bumped version to 2021.4.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9b77b3a4f04..8efde2109ca 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 = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From e760c23f378ce2428998ee084ffbe98d048e7efa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Apr 2021 18:42:23 +0200 Subject: [PATCH 015/120] Include script script_execution in script and automation traces (#48576) --- .../components/automation/__init__.py | 2 + homeassistant/components/trace/__init__.py | 4 + homeassistant/helpers/script.py | 13 +- homeassistant/helpers/trace.py | 27 +++ tests/components/trace/test_websocket_api.py | 166 +++++++++++++++++- tests/helpers/test_script.py | 36 ++-- 6 files changed, 228 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 554b27fdb2f..6caa53dff79 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -57,6 +57,7 @@ from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.trace import ( TraceElement, + script_execution_set, trace_append_element, trace_get, trace_path, @@ -471,6 +472,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): "Conditions not met, aborting automation. Condition summary: %s", trace_get(clear=False), ) + script_execution_set("failed_conditions") return self.async_set_context(trigger_context) diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index cdae44fff6b..c17cbf86715 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -8,6 +8,7 @@ from typing import Any, Deque from homeassistant.core import Context from homeassistant.helpers.trace import ( TraceElement, + script_execution_get, trace_id_get, trace_id_set, trace_set_child_id, @@ -55,6 +56,7 @@ class ActionTrace: self.context: Context = context self._error: Exception | None = None self._state: str = "running" + self._script_execution: str | None = None self.run_id: str = str(next(self._run_ids)) self._timestamp_finish: dt.datetime | None = None self._timestamp_start: dt.datetime = dt_util.utcnow() @@ -75,6 +77,7 @@ class ActionTrace: """Set finish time.""" self._timestamp_finish = dt_util.utcnow() self._state = "stopped" + self._script_execution = script_execution_get() def as_dict(self) -> dict[str, Any]: """Return dictionary version of this ActionTrace.""" @@ -109,6 +112,7 @@ class ActionTrace: "last_step": last_step, "run_id": self.run_id, "state": self._state, + "script_execution": self._script_execution, "timestamp": { "start": self._timestamp_start, "finish": self._timestamp_finish, diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index e342f0ff9a8..bf52fc81b6a 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -63,6 +63,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.script_variables import ScriptVariables +from homeassistant.helpers.trace import script_execution_set from homeassistant.helpers.trigger import ( async_initialize_triggers, async_validate_trigger_config, @@ -332,15 +333,19 @@ class _ScriptRun: async def async_run(self) -> None: """Run script.""" try: - if self._stop.is_set(): - return self._log("Running %s", self._script.running_description) for self._step, self._action in enumerate(self._script.sequence): if self._stop.is_set(): + script_execution_set("cancelled") break await self._async_step(log_exceptions=False) + else: + script_execution_set("finished") except _StopScript: - pass + script_execution_set("aborted") + except Exception: + script_execution_set("error") + raise finally: self._finish() @@ -1137,6 +1142,7 @@ class Script: if self.script_mode == SCRIPT_MODE_SINGLE: if self._max_exceeded != "SILENT": self._log("Already running", level=LOGSEVERITY[self._max_exceeded]) + script_execution_set("failed_single") return if self.script_mode == SCRIPT_MODE_RESTART: self._log("Restarting") @@ -1147,6 +1153,7 @@ class Script: "Maximum number of runs exceeded", level=LOGSEVERITY[self._max_exceeded], ) + script_execution_set("failed_max_runs") return # If this is a top level Script then make a copy of the variables in case they diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 5d5a0f5ff03..c92766036c6 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -88,6 +88,10 @@ variables_cv: ContextVar[Any | None] = ContextVar("variables_cv", default=None) trace_id_cv: ContextVar[tuple[str, str] | None] = ContextVar( "trace_id_cv", default=None ) +# Reason for stopped script execution +script_execution_cv: ContextVar[StopReason | None] = ContextVar( + "script_execution_cv", default=None +) def trace_id_set(trace_id: tuple[str, str]) -> None: @@ -172,6 +176,7 @@ def trace_clear() -> None: trace_stack_cv.set(None) trace_path_stack_cv.set(None) variables_cv.set(None) + script_execution_cv.set(StopReason()) def trace_set_child_id(child_key: tuple[str, str], child_run_id: str) -> None: @@ -187,6 +192,28 @@ def trace_set_result(**kwargs: Any) -> None: node.set_result(**kwargs) +class StopReason: + """Mutable container class for script_execution.""" + + script_execution: str | None = None + + +def script_execution_set(reason: str) -> None: + """Set stop reason.""" + data = script_execution_cv.get() + if data is None: + return + data.script_execution = reason + + +def script_execution_get() -> str | None: + """Return the current trace.""" + data = script_execution_cv.get() + if data is None: + return None + return data.script_execution + + @contextmanager def trace_path(suffix: str | list[str]) -> Generator: """Go deeper in the config tree. diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 84d7100d1c8..8e481dd34b9 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -1,9 +1,11 @@ """Test Trace websocket API.""" +import asyncio + import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components.trace.const import STORED_TRACES -from homeassistant.core import Context +from homeassistant.core import Context, callback from homeassistant.helpers.typing import UNDEFINED from tests.common import assert_lists_same @@ -170,6 +172,7 @@ async def test_get_trace( assert trace["context"] assert trace["error"] == "Unable to find service test.automation" assert trace["state"] == "stopped" + assert trace["script_execution"] == "error" assert trace["item_id"] == "sun" assert trace["context"][context_key] == context.id assert trace.get("trigger", UNDEFINED) == trigger[0] @@ -210,6 +213,7 @@ async def test_get_trace( assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == "finished" assert trace["item_id"] == "moon" assert trace.get("trigger", UNDEFINED) == trigger[1] @@ -260,6 +264,7 @@ async def test_get_trace( assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == "failed_conditions" assert trace["trigger"] == "event 'test_event3'" assert trace["item_id"] == "moon" contexts[trace["context"]["id"]] = { @@ -301,6 +306,7 @@ async def test_get_trace( assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == "finished" assert trace["trigger"] == "event 'test_event2'" assert trace["item_id"] == "moon" contexts[trace["context"]["id"]] = { @@ -391,7 +397,7 @@ async def test_trace_overflow(hass, hass_ws_client, domain): @pytest.mark.parametrize( - "domain, prefix, trigger, last_step", + "domain, prefix, trigger, last_step, script_execution", [ ( "automation", @@ -403,16 +409,20 @@ async def test_trace_overflow(hass, hass_ws_client, domain): "event 'test_event2'", ], ["{prefix}/0", "{prefix}/0", "condition/0", "{prefix}/0"], + ["error", "finished", "failed_conditions", "finished"], ), ( "script", "sequence", [UNDEFINED, UNDEFINED, UNDEFINED, UNDEFINED], ["{prefix}/0", "{prefix}/0", "{prefix}/0", "{prefix}/0"], + ["error", "finished", "finished", "finished"], ), ], ) -async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_step): +async def test_list_traces( + hass, hass_ws_client, domain, prefix, trigger, last_step, script_execution +): """Test listing script and automation traces.""" id = 1 @@ -458,7 +468,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s await _run_automation_or_script(hass, domain, sun_config, "test_event") await hass.async_block_till_done() - # Get trace + # List traces await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) response = await client.receive_json() assert response["success"] @@ -492,7 +502,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s await _run_automation_or_script(hass, domain, moon_config, "test_event2") await hass.async_block_till_done() - # Get trace + # List traces await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) response = await client.receive_json() assert response["success"] @@ -502,6 +512,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s assert trace["last_step"] == last_step[0].format(prefix=prefix) assert trace["error"] == "Unable to find service test.automation" assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution[0] assert trace["timestamp"] assert trace["item_id"] == "sun" assert trace.get("trigger", UNDEFINED) == trigger[0] @@ -510,6 +521,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s assert trace["last_step"] == last_step[1].format(prefix=prefix) assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution[1] assert trace["timestamp"] assert trace["item_id"] == "moon" assert trace.get("trigger", UNDEFINED) == trigger[1] @@ -518,6 +530,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s assert trace["last_step"] == last_step[2].format(prefix=prefix) assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution[2] assert trace["timestamp"] assert trace["item_id"] == "moon" assert trace.get("trigger", UNDEFINED) == trigger[2] @@ -526,6 +539,7 @@ async def test_list_traces(hass, hass_ws_client, domain, prefix, trigger, last_s assert trace["last_step"] == last_step[3].format(prefix=prefix) assert "error" not in trace assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution[3] assert trace["timestamp"] assert trace["item_id"] == "moon" assert trace.get("trigger", UNDEFINED) == trigger[3] @@ -1006,3 +1020,145 @@ async def test_breakpoints_3(hass, hass_ws_client, domain, prefix): "node": f"{prefix}/5", "run_id": run_id, } + + +@pytest.mark.parametrize( + "script_mode,max_runs,script_execution", + [ + ({"mode": "single"}, 1, "failed_single"), + ({"mode": "parallel", "max": 2}, 2, "failed_max_runs"), + ], +) +async def test_script_mode( + hass, hass_ws_client, script_mode, max_runs, script_execution +): + """Test overlapping runs with max_runs > 1.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + flag = asyncio.Event() + + @callback + def _handle_event(_): + flag.set() + + event = "test_event" + script_config = { + "script1": { + "sequence": [ + {"event": event, "event_data": {"value": 1}}, + {"wait_template": "{{ states.switch.test.state == 'off' }}"}, + {"event": event, "event_data": {"value": 2}}, + ], + **script_mode, + }, + } + client = await hass_ws_client() + hass.bus.async_listen(event, _handle_event) + assert await async_setup_component(hass, "script", {"script": script_config}) + + for _ in range(max_runs): + hass.states.async_set("switch.test", "on") + await hass.services.async_call("script", "script1") + await asyncio.wait_for(flag.wait(), 1) + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + traces = _find_traces(response["result"], "script", "script1") + assert len(traces) == max_runs + for trace in traces: + assert trace["state"] == "running" + + # Start additional run of script while first runs are suspended in wait_template. + + flag.clear() + await hass.services.async_call("script", "script1") + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + traces = _find_traces(response["result"], "script", "script1") + assert len(traces) == max_runs + 1 + assert traces[-1]["state"] == "stopped" + assert traces[-1]["script_execution"] == script_execution + + +@pytest.mark.parametrize( + "script_mode,script_execution", + [("restart", "cancelled"), ("parallel", "finished")], +) +async def test_script_mode_2(hass, hass_ws_client, script_mode, script_execution): + """Test overlapping runs with max_runs > 1.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + flag = asyncio.Event() + + @callback + def _handle_event(_): + flag.set() + + event = "test_event" + script_config = { + "script1": { + "sequence": [ + {"event": event, "event_data": {"value": 1}}, + {"wait_template": "{{ states.switch.test.state == 'off' }}"}, + {"event": event, "event_data": {"value": 2}}, + ], + "mode": script_mode, + } + } + client = await hass_ws_client() + hass.bus.async_listen(event, _handle_event) + assert await async_setup_component(hass, "script", {"script": script_config}) + + hass.states.async_set("switch.test", "on") + await hass.services.async_call("script", "script1") + await asyncio.wait_for(flag.wait(), 1) + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + trace = _find_traces(response["result"], "script", "script1")[0] + assert trace["state"] == "running" + + # Start second run of script while first run is suspended in wait_template. + + flag.clear() + await hass.services.async_call("script", "script1") + await asyncio.wait_for(flag.wait(), 1) + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + trace = _find_traces(response["result"], "script", "script1")[1] + assert trace["state"] == "running" + + # Let both scripts finish + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": "script"}) + response = await client.receive_json() + assert response["success"] + trace = _find_traces(response["result"], "script", "script1")[0] + assert trace["state"] == "stopped" + assert trace["script_execution"] == script_execution + trace = _find_traces(response["result"], "script", "script1")[1] + assert trace["state"] == "stopped" + assert trace["script_execution"] == "finished" diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index e4170ccae20..7224dd70677 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -86,9 +86,10 @@ def assert_element(trace_element, expected_element, path): assert not trace_element._variables -def assert_action_trace(expected): +def assert_action_trace(expected, expected_script_execution="finished"): """Assert a trace condition sequence is as expected.""" action_trace = trace.trace_get(clear=False) + script_execution = trace.script_execution_get() trace.trace_clear() expected_trace_keys = list(expected.keys()) assert list(action_trace.keys()) == expected_trace_keys @@ -98,6 +99,8 @@ def assert_action_trace(expected): path = f"[{trace_key_index}][{index}]" assert_element(action_trace[key][index], element, path) + assert script_execution == expected_script_execution + def async_watch_for_action(script_obj, message): """Watch for message in last_action.""" @@ -620,7 +623,8 @@ async def test_delay_template_invalid(hass, caplog): { "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"error_type": script._StopScript}], - } + }, + expected_script_execution="aborted", ) @@ -680,7 +684,8 @@ async def test_delay_template_complex_invalid(hass, caplog): { "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"error_type": script._StopScript}], - } + }, + expected_script_execution="aborted", ) @@ -717,7 +722,8 @@ async def test_cancel_delay(hass): assert_action_trace( { "0": [{"result": {"delay": 5.0, "done": False}}], - } + }, + expected_script_execution="cancelled", ) @@ -969,13 +975,15 @@ async def test_cancel_wait(hass, action_type): assert_action_trace( { "0": [{"result": {"wait": {"completed": False, "remaining": None}}}], - } + }, + expected_script_execution="cancelled", ) else: assert_action_trace( { "0": [{"result": {"wait": {"trigger": None, "remaining": None}}}], - } + }, + expected_script_execution="cancelled", ) @@ -1131,6 +1139,7 @@ async def test_wait_continue_on_timeout( if continue_on_timeout is False: expected_trace["0"][0]["result"]["timeout"] = True expected_trace["0"][0]["error_type"] = script._StopScript + expected_script_execution = "aborted" else: expected_trace["1"] = [ { @@ -1138,7 +1147,8 @@ async def test_wait_continue_on_timeout( "variables": variable_wait, } ] - assert_action_trace(expected_trace) + expected_script_execution = "finished" + assert_action_trace(expected_trace, expected_script_execution) async def test_wait_template_variables_in(hass): @@ -1404,7 +1414,8 @@ async def test_condition_warning(hass, caplog): "1": [{"error_type": script._StopScript, "result": {"result": False}}], "1/condition": [{"error_type": ConditionError}], "1/condition/entity_id/0": [{"error_type": ConditionError}], - } + }, + expected_script_execution="aborted", ) @@ -1456,7 +1467,8 @@ async def test_condition_basic(hass, caplog): "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"error_type": script._StopScript, "result": {"result": False}}], "1/condition": [{"result": {"result": False}}], - } + }, + expected_script_execution="aborted", ) @@ -2141,7 +2153,7 @@ async def test_propagate_error_service_not_found(hass): } ], } - assert_action_trace(expected_trace) + assert_action_trace(expected_trace, expected_script_execution="error") async def test_propagate_error_invalid_service_data(hass): @@ -2178,7 +2190,7 @@ async def test_propagate_error_invalid_service_data(hass): } ], } - assert_action_trace(expected_trace) + assert_action_trace(expected_trace, expected_script_execution="error") async def test_propagate_error_service_exception(hass): @@ -2219,7 +2231,7 @@ async def test_propagate_error_service_exception(hass): } ], } - assert_action_trace(expected_trace) + assert_action_trace(expected_trace, expected_script_execution="error") async def test_referenced_entities(hass): From 231a55d416302c341b6988f33eb2191d485c0436 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Apr 2021 22:34:47 +0200 Subject: [PATCH 016/120] Include blueprint input in automation trace (#48575) --- .../components/automation/__init__.py | 11 ++- homeassistant/components/automation/trace.py | 7 +- homeassistant/components/script/trace.py | 2 +- homeassistant/components/trace/__init__.py | 3 + tests/components/trace/test_websocket_api.py | 70 +++++++++++++++++++ 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 6caa53dff79..36b7f1688f8 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -273,6 +273,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): variables, trigger_variables, raw_config, + blueprint_inputs, ): """Initialize an automation entity.""" self._id = automation_id @@ -290,6 +291,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._variables: ScriptVariables = variables self._trigger_variables: ScriptVariables = trigger_variables self._raw_config = raw_config + self._blueprint_inputs = blueprint_inputs @property def name(self): @@ -437,7 +439,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity): trigger_context = Context(parent_id=parent_id) with trace_automation( - self.hass, self.unique_id, self._raw_config, trigger_context + self.hass, + self.unique_id, + self._raw_config, + self._blueprint_inputs, + trigger_context, ) as automation_trace: if self._variables: try: @@ -603,10 +609,12 @@ async def _async_process_config( ] for list_no, config_block in enumerate(conf): + raw_blueprint_inputs = None raw_config = None if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore blueprints_used = True blueprint_inputs = config_block + raw_blueprint_inputs = blueprint_inputs.config_with_inputs try: raw_config = blueprint_inputs.async_substitute() @@ -675,6 +683,7 @@ async def _async_process_config( variables, config_block.get(CONF_TRIGGER_VARIABLES), raw_config, + raw_blueprint_inputs, ) entities.append(entity) diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 0b335f7d87f..cfdbe02056b 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -18,11 +18,12 @@ class AutomationTrace(ActionTrace): self, item_id: str, config: dict[str, Any], + blueprint_inputs: dict[str, Any], context: Context, ): """Container for automation trace.""" key = ("automation", item_id) - super().__init__(key, config, context) + super().__init__(key, config, blueprint_inputs, context) self._trigger_description: str | None = None def set_trigger_description(self, trigger: str) -> None: @@ -37,9 +38,9 @@ class AutomationTrace(ActionTrace): @contextmanager -def trace_automation(hass, automation_id, config, context): +def trace_automation(hass, automation_id, config, blueprint_inputs, context): """Trace action execution of automation with automation_id.""" - trace = AutomationTrace(automation_id, config, context) + trace = AutomationTrace(automation_id, config, blueprint_inputs, context) async_store_trace(hass, trace) try: diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py index 1a7cc01e084..a8053feaa1e 100644 --- a/homeassistant/components/script/trace.py +++ b/homeassistant/components/script/trace.py @@ -19,7 +19,7 @@ class ScriptTrace(ActionTrace): ): """Container for automation trace.""" key = ("script", item_id) - super().__init__(key, config, context) + super().__init__(key, config, None, context) @contextmanager diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index c17cbf86715..eca22a56da8 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -48,11 +48,13 @@ class ActionTrace: self, key: tuple[str, str], config: dict[str, Any], + blueprint_inputs: dict[str, Any], context: Context, ): """Container for script trace.""" self._trace: dict[str, Deque[TraceElement]] | None = None self._config: dict[str, Any] = config + self._blueprint_inputs: dict[str, Any] = blueprint_inputs self.context: Context = context self._error: Exception | None = None self._state: str = "running" @@ -93,6 +95,7 @@ class ActionTrace: { "trace": traces, "config": self._config, + "blueprint_inputs": self._blueprint_inputs, "context": self.context, } ) diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 8e481dd34b9..0b7b78b3f1a 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -169,6 +169,7 @@ async def test_get_trace( assert trace["trace"][f"{prefix}/0"][0]["error"] assert trace["trace"][f"{prefix}/0"][0]["result"] == sun_action _assert_raw_config(domain, sun_config, trace) + assert trace["blueprint_inputs"] is None assert trace["context"] assert trace["error"] == "Unable to find service test.automation" assert trace["state"] == "stopped" @@ -210,6 +211,7 @@ async def test_get_trace( assert "error" not in trace["trace"][f"{prefix}/0"][0] assert trace["trace"][f"{prefix}/0"][0]["result"] == moon_action _assert_raw_config(domain, moon_config, trace) + assert trace["blueprint_inputs"] is None assert trace["context"] assert "error" not in trace assert trace["state"] == "stopped" @@ -1162,3 +1164,71 @@ async def test_script_mode_2(hass, hass_ws_client, script_mode, script_execution trace = _find_traces(response["result"], "script", "script1")[1] assert trace["state"] == "stopped" assert trace["script_execution"] == "finished" + + +async def test_trace_blueprint_automation(hass, hass_ws_client): + """Test trace of blueprint automation.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + domain = "automation" + sun_config = { + "id": "sun", + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "blueprint_event", + "service_to_call": "test.automation", + }, + }, + } + sun_action = { + "limit": 10, + "params": { + "domain": "test", + "service": "automation", + "service_data": {}, + "target": {"entity_id": ["light.kitchen"]}, + }, + "running_script": False, + } + assert await async_setup_component(hass, "automation", {"automation": sun_config}) + client = await hass_ws_client() + hass.bus.async_fire("blueprint_event") + await hass.async_block_till_done() + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) + response = await client.receive_json() + assert response["success"] + run_id = _find_run_id(response["result"], domain, "sun") + + # Get trace + await client.send_json( + { + "id": next_id(), + "type": "trace/get", + "domain": domain, + "item_id": "sun", + "run_id": run_id, + } + ) + response = await client.receive_json() + assert response["success"] + trace = response["result"] + assert set(trace["trace"]) == {"trigger/0", "action/0"} + assert len(trace["trace"]["action/0"]) == 1 + assert trace["trace"]["action/0"][0]["error"] + assert trace["trace"]["action/0"][0]["result"] == sun_action + assert trace["config"]["id"] == "sun" + assert trace["blueprint_inputs"] == sun_config + assert trace["context"] + assert trace["error"] == "Unable to find service test.automation" + assert trace["state"] == "stopped" + assert trace["script_execution"] == "error" + assert trace["item_id"] == "sun" + assert trace.get("trigger", UNDEFINED) == "event 'blueprint_event'" From 74357d97602f3c0994536b789d446e5f6cbb9c15 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Apr 2021 23:33:37 +0000 Subject: [PATCH 017/120] Bumped version to 2021.4.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8efde2109ca..cf6c9257e0c 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 = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From cec80210a3e8569b09722b32130c68a89cd078d8 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 2 Apr 2021 12:48:57 +0300 Subject: [PATCH 018/120] Bump aioshelly to 0.6.2 (#48620) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index a757947c5cf..1ae274d6dfd 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.6.1"], + "requirements": ["aioshelly==0.6.2"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } diff --git a/requirements_all.txt b/requirements_all.txt index 278ae107af6..c768ab8a222 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.6.1 +aioshelly==0.6.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8fdcceedf8..f48fd5ed973 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.6.1 +aioshelly==0.6.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From da313281505c5820c51147f17eb9fa137b0e4e18 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Apr 2021 10:24:38 -0700 Subject: [PATCH 019/120] Fix trigger template entities without a unique ID (#48631) --- homeassistant/components/template/sensor.py | 2 ++ tests/components/template/test_sensor.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 9714d147e01..a5f5d669b16 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASSES_SCHEMA, + DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SensorEntity, @@ -201,6 +202,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): class TriggerSensorEntity(TriggerEntity, SensorEntity): """Sensor entity based on trigger data.""" + domain = SENSOR_DOMAIN extra_template_keys = (CONF_VALUE_TEMPLATE,) @property diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 11945a3b027..6aa1e75cc1f 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1014,7 +1014,15 @@ async def test_trigger_entity(hass): "attribute_templates": { "plus_one": "{{ trigger.event.data.beer + 1 }}" }, - } + }, + }, + }, + { + "trigger": [], + "sensors": { + "bare_minimum": { + "value_template": "{{ trigger.event.data.beer }}" + }, }, }, ], @@ -1027,6 +1035,10 @@ async def test_trigger_entity(hass): assert state is not None assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.bare_minimum") + assert state is not None + assert state.state == STATE_UNKNOWN + context = Context() hass.bus.async_fire("test_event", {"beer": 2}, context=context) await hass.async_block_till_done() From 2a1f6d7e8ff1283d02108f7e5bb0f3b83c3f176e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Apr 2021 16:57:16 -0700 Subject: [PATCH 020/120] Support modern config for the trigger based template entity (#48635) --- homeassistant/components/template/__init__.py | 20 ++-- homeassistant/components/template/config.py | 91 +++++++++++++++++-- homeassistant/components/template/const.py | 4 + homeassistant/components/template/sensor.py | 11 ++- .../components/template/trigger_entity.py | 79 +++++++--------- homeassistant/helpers/template.py | 4 +- tests/components/template/test_sensor.py | 40 ++++++-- 7 files changed, 173 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 3481b5adac6..f9b6b3b4975 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -2,7 +2,8 @@ import logging from typing import Optional -from homeassistant.const import CONF_SENSORS, EVENT_HOMEASSISTANT_START +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import CoreState, callback from homeassistant.helpers import ( discovery, @@ -51,15 +52,16 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): EVENT_HOMEASSISTANT_START, self._attach_triggers ) - self.hass.async_create_task( - discovery.async_load_platform( - self.hass, - "sensor", - DOMAIN, - {"coordinator": self, "entities": self.config[CONF_SENSORS]}, - hass_config, + for platform_domain in (SENSOR_DOMAIN,): + self.hass.async_create_task( + discovery.async_load_platform( + self.hass, + platform_domain, + DOMAIN, + {"coordinator": self, "entities": self.config[platform_domain]}, + hass_config, + ) ) - ) async def _attach_triggers(self, start_event=None) -> None: """Attach the triggers.""" diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index fa0d9a867d1..edef5673f31 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -1,23 +1,72 @@ """Template config validator.""" +import logging import voluptuous as vol +from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, + DOMAIN as SENSOR_DOMAIN, +) from homeassistant.config import async_log_exception, config_without_domain -from homeassistant.const import CONF_SENSORS, CONF_UNIQUE_ID -from homeassistant.helpers import config_validation as cv +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME, + CONF_FRIENDLY_NAME_TEMPLATE, + CONF_ICON, + CONF_ICON_TEMPLATE, + CONF_NAME, + CONF_SENSORS, + CONF_STATE, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, +) +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.trigger import async_validate_trigger_config -from .const import CONF_TRIGGER, DOMAIN -from .sensor import SENSOR_SCHEMA +from .const import ( + CONF_ATTRIBUTE_TEMPLATES, + CONF_ATTRIBUTES, + CONF_AVAILABILITY, + CONF_AVAILABILITY_TEMPLATE, + CONF_PICTURE, + CONF_TRIGGER, + DOMAIN, +) +from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA -CONF_STATE = "state" +CONVERSION_PLATFORM = { + CONF_ICON_TEMPLATE: CONF_ICON, + CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, + CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, + CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES, + CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME, + CONF_FRIENDLY_NAME: CONF_NAME, + CONF_VALUE_TEMPLATE: CONF_STATE, +} +SENSOR_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_AVAILABILITY): cv.template, + vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) + TRIGGER_ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, - vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), + vol.Optional(SENSOR_DOMAIN): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), + vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(PLATFORM_SENSOR_SCHEMA), } ) @@ -37,9 +86,37 @@ async def async_validate_config(hass, config): ) except vol.Invalid as err: async_log_exception(err, DOMAIN, cfg, hass) + continue - else: + if CONF_SENSORS not in cfg: trigger_entity_configs.append(cfg) + continue + + logging.getLogger(__name__).warning( + "The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" + ) + sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] + + for device_id, entity_cfg in cfg[CONF_SENSORS].items(): + entity_cfg = {**entity_cfg} + + for from_key, to_key in CONVERSION_PLATFORM.items(): + if from_key not in entity_cfg or to_key in entity_cfg: + continue + + val = entity_cfg.pop(from_key) + if isinstance(val, str): + val = template.Template(val) + entity_cfg[to_key] = val + + if CONF_NAME not in entity_cfg: + entity_cfg[CONF_NAME] = template.Template(device_id) + + sensor.append(entity_cfg) + + cfg = {**cfg, "sensor": sensor} + + trigger_entity_configs.append(cfg) # Create a copy of the configuration with all config for current # component removed and add validated config back in. diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 2f2bc3127d7..971d4a864c9 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -20,3 +20,7 @@ PLATFORMS = [ "vacuum", "weather", ] + +CONF_AVAILABILITY = "availability" +CONF_ATTRIBUTES = "attributes" +CONF_PICTURE = "picture" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a5f5d669b16..4631a775847 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_FRIENDLY_NAME_TEMPLATE, CONF_ICON_TEMPLATE, CONF_SENSORS, + CONF_STATE, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, @@ -89,7 +90,7 @@ def _async_create_template_tracking_entities(hass, config): friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(CONF_UNIT_OF_MEASUREMENT) device_class = device_config.get(CONF_DEVICE_CLASS) - attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES] + attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) unique_id = device_config.get(CONF_UNIQUE_ID) sensors.append( @@ -118,8 +119,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(_async_create_template_tracking_entities(hass, config)) else: async_add_entities( - TriggerSensorEntity(hass, discovery_info["coordinator"], device_id, config) - for device_id, config in discovery_info["entities"].items() + TriggerSensorEntity(hass, discovery_info["coordinator"], config) + for config in discovery_info["entities"] ) @@ -203,9 +204,9 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): """Sensor entity based on trigger data.""" domain = SENSOR_DOMAIN - extra_template_keys = (CONF_VALUE_TEMPLATE,) + extra_template_keys = (CONF_STATE,) @property def state(self) -> str | None: """Return state of the sensor.""" - return self._rendered.get(CONF_VALUE_TEMPLATE) + return self._rendered.get(CONF_STATE) diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 3874409dc78..418fa976304 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -6,20 +6,16 @@ from typing import Any from homeassistant.const import ( CONF_DEVICE_CLASS, - CONF_ENTITY_PICTURE_TEMPLATE, - CONF_FRIENDLY_NAME, - CONF_FRIENDLY_NAME_TEMPLATE, - CONF_ICON_TEMPLATE, + CONF_ICON, + CONF_NAME, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import template, update_coordinator -from homeassistant.helpers.entity import async_generate_entity_id from . import TriggerUpdateCoordinator -from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE +from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE class TriggerEntity(update_coordinator.CoordinatorEntity): @@ -32,23 +28,13 @@ class TriggerEntity(update_coordinator.CoordinatorEntity): self, hass: HomeAssistant, coordinator: TriggerUpdateCoordinator, - device_id: str, config: dict, ): """Initialize the entity.""" super().__init__(coordinator) - self.entity_id = async_generate_entity_id( - self.domain + ".{}", device_id, hass=hass - ) - - self._name = config.get(CONF_FRIENDLY_NAME, device_id) - entity_unique_id = config.get(CONF_UNIQUE_ID) - if entity_unique_id is None and coordinator.unique_id: - entity_unique_id = device_id - if entity_unique_id and coordinator.unique_id: self._unique_id = f"{coordinator.unique_id}-{entity_unique_id}" else: @@ -56,32 +42,33 @@ class TriggerEntity(update_coordinator.CoordinatorEntity): self._config = config - self._to_render = [ - itm - for itm in ( - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, - CONF_ENTITY_PICTURE_TEMPLATE, - CONF_FRIENDLY_NAME_TEMPLATE, - CONF_AVAILABILITY_TEMPLATE, - ) - if itm in config - ] + self._static_rendered = {} + self._to_render = [] + + for itm in ( + CONF_NAME, + CONF_ICON, + CONF_PICTURE, + CONF_AVAILABILITY, + ): + if itm not in config: + continue + + if config[itm].is_static: + self._static_rendered[itm] = config[itm].template + else: + self._to_render.append(itm) if self.extra_template_keys is not None: self._to_render.extend(self.extra_template_keys) - self._rendered = {} + # We make a copy so our initial render is 'unknown' and not 'unavailable' + self._rendered = dict(self._static_rendered) @property def name(self): """Name of the entity.""" - if ( - self._rendered is not None - and (name := self._rendered.get(CONF_FRIENDLY_NAME_TEMPLATE)) is not None - ): - return name - return self._name + return self._rendered.get(CONF_NAME) @property def unique_id(self): @@ -101,29 +88,27 @@ class TriggerEntity(update_coordinator.CoordinatorEntity): @property def icon(self) -> str | None: """Return icon.""" - return self._rendered is not None and self._rendered.get(CONF_ICON_TEMPLATE) + return self._rendered.get(CONF_ICON) @property def entity_picture(self) -> str | None: """Return entity picture.""" - return self._rendered is not None and self._rendered.get( - CONF_ENTITY_PICTURE_TEMPLATE - ) + return self._rendered.get(CONF_PICTURE) @property def available(self): """Return availability of the entity.""" return ( - self._rendered is not None + self._rendered is not self._static_rendered and # Check against False so `None` is ok - self._rendered.get(CONF_AVAILABILITY_TEMPLATE) is not False + self._rendered.get(CONF_AVAILABILITY) is not False ) @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return extra attributes.""" - return self._rendered.get(CONF_ATTRIBUTE_TEMPLATES) + return self._rendered.get(CONF_ATTRIBUTES) async def async_added_to_hass(self) -> None: """Handle being added to Home Assistant.""" @@ -136,16 +121,16 @@ class TriggerEntity(update_coordinator.CoordinatorEntity): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" try: - rendered = {} + rendered = dict(self._static_rendered) for key in self._to_render: rendered[key] = self._config[key].async_render( self.coordinator.data["run_variables"], parse_result=False ) - if CONF_ATTRIBUTE_TEMPLATES in self._config: - rendered[CONF_ATTRIBUTE_TEMPLATES] = template.render_complex( - self._config[CONF_ATTRIBUTE_TEMPLATES], + if CONF_ATTRIBUTES in self._config: + rendered[CONF_ATTRIBUTES] = template.render_complex( + self._config[CONF_ATTRIBUTES], self.coordinator.data["run_variables"], ) @@ -154,7 +139,7 @@ class TriggerEntity(update_coordinator.CoordinatorEntity): logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error( "Error rendering %s template for %s: %s", key, self.entity_id, err ) - self._rendered = None + self._rendered = self._static_rendered self.async_set_context(self.coordinator.data["context"]) self.async_write_ha_state() diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 315efd14516..4989c4172ae 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -336,7 +336,7 @@ class Template: If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. """ if self.is_static: - if self.hass.config.legacy_templates or not parse_result: + if not parse_result or self.hass.config.legacy_templates: return self.template return self._parse_result(self.template) @@ -360,7 +360,7 @@ class Template: If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. """ if self.is_static: - if self.hass.config.legacy_templates or not parse_result: + if not parse_result or self.hass.config.legacy_templates: return self.template return self._parse_result(self.template) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 6aa1e75cc1f..d146f5d88de 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -998,14 +998,14 @@ async def test_trigger_entity(hass): { "template": [ {"invalid": "config"}, - # This one should still be set up + # Config after invalid should still be set up { "unique_id": "listening-test-event", "trigger": {"platform": "event", "event_type": "test_event"}, "sensors": { "hello": { "friendly_name": "Hello Name", - "unique_id": "just_a_test", + "unique_id": "hello_name-id", "device_class": "battery", "unit_of_measurement": "%", "value_template": "{{ trigger.event.data.beer }}", @@ -1016,6 +1016,20 @@ async def test_trigger_entity(hass): }, }, }, + "sensor": [ + { + "name": "via list", + "unique_id": "via_list-id", + "device_class": "battery", + "unit_of_measurement": "%", + "state": "{{ trigger.event.data.beer + 1 }}", + "picture": "{{ '/local/dogs.png' }}", + "icon": "{{ 'mdi:pirate' }}", + "attributes": { + "plus_one": "{{ trigger.event.data.beer + 1 }}" + }, + } + ], }, { "trigger": [], @@ -1031,7 +1045,7 @@ async def test_trigger_entity(hass): await hass.async_block_till_done() - state = hass.states.get("sensor.hello") + state = hass.states.get("sensor.hello_name") assert state is not None assert state.state == STATE_UNKNOWN @@ -1043,7 +1057,7 @@ async def test_trigger_entity(hass): hass.bus.async_fire("test_event", {"beer": 2}, context=context) await hass.async_block_till_done() - state = hass.states.get("sensor.hello") + state = hass.states.get("sensor.hello_name") assert state.state == "2" assert state.attributes.get("device_class") == "battery" assert state.attributes.get("icon") == "mdi:pirate" @@ -1053,10 +1067,24 @@ async def test_trigger_entity(hass): assert state.context is context ent_reg = entity_registry.async_get(hass) - assert len(ent_reg.entities) == 1 + assert len(ent_reg.entities) == 2 assert ( - ent_reg.entities["sensor.hello"].unique_id == "listening-test-event-just_a_test" + ent_reg.entities["sensor.hello_name"].unique_id + == "listening-test-event-hello_name-id" ) + assert ( + ent_reg.entities["sensor.via_list"].unique_id + == "listening-test-event-via_list-id" + ) + + state = hass.states.get("sensor.via_list") + assert state.state == "3" + assert state.attributes.get("device_class") == "battery" + assert state.attributes.get("icon") == "mdi:pirate" + assert state.attributes.get("entity_picture") == "/local/dogs.png" + assert state.attributes.get("plus_one") == 3 + assert state.attributes.get("unit_of_measurement") == "%" + assert state.context is context async def test_trigger_entity_render_error(hass): From 396a8a3a1048f57cc311ef2c6f45538ee8eaf008 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Apr 2021 23:11:39 +0200 Subject: [PATCH 021/120] Updated frontend to 20210402.1 (#48639) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 60ea0ff53b2..55392323f3d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210402.0" + "home-assistant-frontend==20210402.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a2a9e5eac2c..6571f3f0f57 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210402.0 +home-assistant-frontend==20210402.1 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index c768ab8a222..9ba4b09ad7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210402.0 +home-assistant-frontend==20210402.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f48fd5ed973..cb2501f9693 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210402.0 +home-assistant-frontend==20210402.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From dcb43b474fd05668b344d191275db84b3c6ef71f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 Apr 2021 00:05:20 +0000 Subject: [PATCH 022/120] Bumped version to 2021.4.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cf6c9257e0c..ed05cf85c16 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 = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 311f624adc76c2a69dab060744f8e481f1cc3323 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Apr 2021 23:33:45 -1000 Subject: [PATCH 023/120] Bump aiodiscover to 1.3.3 for dhcp (#48644) fixes #48615 --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 817ee9acac5..80cc6b116c9 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -3,7 +3,7 @@ "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", "requirements": [ - "scapy==2.4.4", "aiodiscover==1.3.2" + "scapy==2.4.4", "aiodiscover==1.3.3" ], "codeowners": [ "@bdraco" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6571f3f0f57..64801ad1afb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiodiscover==1.3.2 +aiodiscover==1.3.3 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==1.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9ba4b09ad7f..468c8d0113f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioazuredevops==1.3.5 aiobotocore==0.11.1 # homeassistant.components.dhcp -aiodiscover==1.3.2 +aiodiscover==1.3.3 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb2501f9693..48e1d3bb2c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ aioazuredevops==1.3.5 aiobotocore==0.11.1 # homeassistant.components.dhcp -aiodiscover==1.3.2 +aiodiscover==1.3.3 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 9eb439783758688d81b263f0754bbe36e984dc70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Apr 2021 14:11:32 -1000 Subject: [PATCH 024/120] Only listen for zeroconf when the esphome device cannot connect (#48645) --- homeassistant/components/esphome/__init__.py | 30 +++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 02d6309fe7f..0caf00af8ef 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -239,6 +239,8 @@ class ReconnectLogic(RecordUpdateListener): # Flag to check if the device is connected self._connected = True self._connected_lock = asyncio.Lock() + self._zc_lock = asyncio.Lock() + self._zc_listening = False # Event the different strategies use for issuing a reconnect attempt. self._reconnect_event = asyncio.Event() # The task containing the infinite reconnect loop while running @@ -270,6 +272,7 @@ class ReconnectLogic(RecordUpdateListener): self._entry_data.disconnect_callbacks = [] self._entry_data.available = False self._entry_data.async_update_device_state(self._hass) + await self._start_zc_listen() # Reset tries async with self._tries_lock: @@ -315,6 +318,7 @@ class ReconnectLogic(RecordUpdateListener): self._host, error, ) + await self._start_zc_listen() # Schedule re-connect in event loop in order not to delay HA # startup. First connect is scheduled in tracked tasks. async with self._wait_task_lock: @@ -332,6 +336,7 @@ class ReconnectLogic(RecordUpdateListener): self._tries = 0 async with self._connected_lock: self._connected = True + await self._stop_zc_listen() self._hass.async_create_task(self._on_login()) async def _reconnect_once(self): @@ -375,9 +380,6 @@ class ReconnectLogic(RecordUpdateListener): # Create reconnection loop outside of HA's tracked tasks in order # not to delay startup. self._loop_task = self._hass.loop.create_task(self._reconnect_loop()) - # Listen for mDNS records so we can reconnect directly if a received mDNS record - # indicates the node is up again - await self._hass.async_add_executor_job(self._zc.add_listener, self, None) async with self._connected_lock: self._connected = False @@ -388,11 +390,31 @@ class ReconnectLogic(RecordUpdateListener): if self._loop_task is not None: self._loop_task.cancel() self._loop_task = None - await self._hass.async_add_executor_job(self._zc.remove_listener, self) async with self._wait_task_lock: if self._wait_task is not None: self._wait_task.cancel() self._wait_task = None + await self._stop_zc_listen() + + async def _start_zc_listen(self): + """Listen for mDNS records. + + This listener allows us to schedule a reconnect as soon as a + received mDNS record indicates the node is up again. + """ + async with self._zc_lock: + if not self._zc_listening: + await self._hass.async_add_executor_job( + self._zc.add_listener, self, None + ) + self._zc_listening = True + + async def _stop_zc_listen(self): + """Stop listening for zeroconf updates.""" + async with self._zc_lock: + if self._zc_listening: + await self._hass.async_add_executor_job(self._zc.remove_listener, self) + self._zc_listening = False @callback def stop_callback(self): From a8cd6228cf338ccf918dcffefee239080e67abd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 3 Apr 2021 11:17:17 +0200 Subject: [PATCH 025/120] Fix AEMET town timestamp format (#48647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Datetime should be converted to ISO format. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/weather_update_coordinator.py | 2 +- tests/components/aemet/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index a9af8f25f1c..a7ca0a12422 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -283,7 +283,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): temperature_feeling = None town_id = None town_name = None - town_timestamp = dt_util.as_utc(elaborated) + town_timestamp = dt_util.as_utc(elaborated).isoformat() wind_bearing = None wind_max_speed = None wind_speed = None diff --git a/tests/components/aemet/test_sensor.py b/tests/components/aemet/test_sensor.py index b265b996709..7887139a386 100644 --- a/tests/components/aemet/test_sensor.py +++ b/tests/components/aemet/test_sensor.py @@ -127,7 +127,7 @@ async def test_aemet_weather_create_sensors(hass): assert state.state == "Getafe" state = hass.states.get("sensor.aemet_town_timestamp") - assert state.state == "2021-01-09 11:47:45+00:00" + assert state.state == "2021-01-09T11:47:45+00:00" state = hass.states.get("sensor.aemet_wind_bearing") assert state.state == "90.0" From 7b1ea46653b2857707441a73ac98ad637dd235b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Apr 2021 14:00:22 -1000 Subject: [PATCH 026/120] Prevent config entry retry from blocking startup (#48660) - If there are two integrations doing long retries async_block_till_done() will never be done --- homeassistant/config_entries.py | 16 +++++++++++----- tests/test_config_entries.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 65cae7942a6..23758cf88f2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,7 +11,8 @@ import weakref import attr from homeassistant import data_entry_flow, loader -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event @@ -276,14 +277,19 @@ class ConfigEntry: wait_time, ) - async def setup_again(now: Any) -> None: + async def setup_again(*_: Any) -> None: """Run setup again.""" self._async_cancel_retry_setup = None await self.async_setup(hass, integration=integration, tries=tries) - self._async_cancel_retry_setup = hass.helpers.event.async_call_later( - wait_time, setup_again - ) + if hass.state == CoreState.running: + self._async_cancel_retry_setup = hass.helpers.event.async_call_later( + wait_time, setup_again + ) + else: + self._async_cancel_retry_setup = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, setup_again + ) return except Exception: # pylint: disable=broad-except _LOGGER.exception( diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 4db1952dbfb..c35ba61a767 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6,7 +6,8 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -904,6 +905,33 @@ async def test_setup_retrying_during_unload(hass): assert len(mock_call.return_value.mock_calls) == 1 +async def test_setup_retrying_during_unload_before_started(hass): + """Test if we unload an entry that is in retry mode before started.""" + entry = MockConfigEntry(domain="test") + hass.state = CoreState.starting + initial_listeners = hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] + + mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert ( + hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 1 + ) + + await entry.async_unload(hass) + await hass.async_block_till_done() + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert ( + hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 0 + ) + + async def test_entry_options(hass, manager): """Test that we can set options on an entry.""" entry = MockConfigEntry(domain="test", data={"first": True}, options=None) From 1850b92b36356be0efaed0553b0acd3131fe4675 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 Apr 2021 00:36:15 +0000 Subject: [PATCH 027/120] Bumped version to 2021.4.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ed05cf85c16..f1bedbb5e6a 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 = "0b3" +PATCH_VERSION = "0b4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From edf41e84258408196d7640c405ace3d8ffdcc0a8 Mon Sep 17 00:00:00 2001 From: mburget <77898400+mburget@users.noreply.github.com> Date: Sun, 4 Apr 2021 12:22:43 +0200 Subject: [PATCH 028/120] Fix Raspi GPIO binary_sensor produces unreliable responses (#48170) * Fix for issue #10498 Raspi GPIO binary_sensor produces unreliable responses ("Doorbell Scenario") Changes overtaken from PR#31788 which was somehow never finished * Fix for issue #10498 Raspi GPIO binary_sensor produces unreliable response. Changes taken over from PR31788 which was somehow never finished * Remove unused code (pylint warning) --- .../components/rpi_gpio/binary_sensor.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py index 36d7ae50f32..318b29131b6 100644 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -1,4 +1,7 @@ """Support for binary sensor using RPi GPIO.""" + +import asyncio + import voluptuous as vol from homeassistant.components import rpi_gpio @@ -52,6 +55,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class RPiGPIOBinarySensor(BinarySensorEntity): """Represent a binary sensor that uses Raspberry Pi GPIO.""" + async def async_read_gpio(self): + """Read state from GPIO.""" + await asyncio.sleep(float(self._bouncetime) / 1000) + self._state = await self.hass.async_add_executor_job( + rpi_gpio.read_input, self._port + ) + self.async_write_ha_state() + def __init__(self, name, port, pull_mode, bouncetime, invert_logic): """Initialize the RPi binary sensor.""" self._name = name or DEVICE_DEFAULT_NAME @@ -63,12 +74,11 @@ class RPiGPIOBinarySensor(BinarySensorEntity): rpi_gpio.setup_input(self._port, self._pull_mode) - def read_gpio(port): - """Read state from GPIO.""" - self._state = rpi_gpio.read_input(self._port) - self.schedule_update_ha_state() + def edge_detected(port): + """Edge detection handler.""" + self.hass.add_job(self.async_read_gpio) - rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime) + rpi_gpio.edge_detect(self._port, edge_detected, self._bouncetime) @property def should_poll(self): From c512ab7ec9f575d2bd99f96963210d32d5a80155 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 5 Apr 2021 00:21:47 -0400 Subject: [PATCH 029/120] Implement Ignore list for poll control configuration on Ikea devices (#48667) Co-authored-by: Hmmbob <33529490+hmmbob@users.noreply.github.com> --- .../components/zha/core/channels/general.py | 11 ++++++- tests/components/zha/test_channels.py | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 626596e1a3e..6ef0bd9e665 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -391,6 +391,9 @@ class PollControl(ZigbeeChannel): CHECKIN_INTERVAL = 55 * 60 * 4 # 55min CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s LONG_POLL = 6 * 4 # 6s + _IGNORED_MANUFACTURER_ID = { + 4476, + } # IKEA async def async_configure_channel_specific(self) -> None: """Configure channel: set check-in interval.""" @@ -416,7 +419,13 @@ class PollControl(ZigbeeChannel): async def check_in_response(self, tsn: int) -> None: """Respond to checkin command.""" await self.checkin_response(True, self.CHECKIN_FAST_POLL_TIMEOUT, tsn=tsn) - await self.set_long_poll_interval(self.LONG_POLL) + if self._ch_pool.manufacturer_code not in self._IGNORED_MANUFACTURER_ID: + await self.set_long_poll_interval(self.LONG_POLL) + + @callback + def skip_manufacturer_id(self, manufacturer_code: int) -> None: + """Block a specific manufacturer id from changing default polling.""" + self._IGNORED_MANUFACTURER_ID.add(manufacturer_code) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id) diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index ec5128fdb5e..a391439a239 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -492,6 +492,38 @@ async def test_poll_control_cluster_command(hass, poll_control_device): assert data["device_id"] == poll_control_device.device_id +async def test_poll_control_ignore_list(hass, poll_control_device): + """Test poll control channel ignore list.""" + set_long_poll_mock = AsyncMock() + poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"] + cluster = poll_control_ch.cluster + + with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): + await poll_control_ch.check_in_response(33) + + assert set_long_poll_mock.call_count == 1 + + set_long_poll_mock.reset_mock() + poll_control_ch.skip_manufacturer_id(4151) + with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): + await poll_control_ch.check_in_response(33) + + assert set_long_poll_mock.call_count == 0 + + +async def test_poll_control_ikea(hass, poll_control_device): + """Test poll control channel ignore list for ikea.""" + set_long_poll_mock = AsyncMock() + poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"] + cluster = poll_control_ch.cluster + + poll_control_device.device.node_desc.manufacturer_code = 4476 + with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): + await poll_control_ch.check_in_response(33) + + assert set_long_poll_mock.call_count == 0 + + @pytest.fixture def zigpy_zll_device(zigpy_device_mock): """ZLL device fixture.""" From bdd68cd413cded52f097c6cc5ef26ce14f5500bb Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 4 Apr 2021 16:09:07 -0400 Subject: [PATCH 030/120] Bump zwave_js dependency to 0.23.1 (#48682) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 4d3f5c5f42d..e6b4ed7c2a8 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -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.23.0"], + "requirements": ["zwave-js-server-python==0.23.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/requirements_all.txt b/requirements_all.txt index 468c8d0113f..72632ce227d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2402,4 +2402,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.23.0 +zwave-js-server-python==0.23.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 48e1d3bb2c0..07f10ebccd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1251,4 +1251,4 @@ zigpy-znp==0.4.0 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.23.0 +zwave-js-server-python==0.23.1 From 26b9017905c0ba0cf20574e88f3968d74fec2e0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 Apr 2021 03:22:25 -0700 Subject: [PATCH 031/120] Fix verisure deadlock (#48691) --- homeassistant/components/verisure/camera.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index c97d7f8c76c..cb159027c16 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -36,7 +36,7 @@ async def async_setup_entry( assert hass.config.config_dir async_add_entities( - VerisureSmartcam(hass, coordinator, serial_number, hass.config.config_dir) + VerisureSmartcam(coordinator, serial_number, hass.config.config_dir) for serial_number in coordinator.data["cameras"] ) @@ -48,7 +48,6 @@ class VerisureSmartcam(CoordinatorEntity, Camera): def __init__( self, - hass: HomeAssistant, coordinator: VerisureDataUpdateCoordinator, serial_number: str, directory_path: str, @@ -60,7 +59,6 @@ class VerisureSmartcam(CoordinatorEntity, Camera): self._directory_path = directory_path self._image = None self._image_id = None - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image) @property def name(self) -> str: @@ -126,7 +124,7 @@ class VerisureSmartcam(CoordinatorEntity, Camera): self._image_id = new_image_id self._image = new_image_path - def delete_image(self) -> None: + def delete_image(self, _=None) -> None: """Delete an old image.""" remove_image = os.path.join( self._directory_path, "{}{}".format(self._image_id, ".jpg") @@ -145,3 +143,8 @@ class VerisureSmartcam(CoordinatorEntity, Camera): LOGGER.debug("Capturing new image from %s", self.serial_number) except VerisureError as ex: LOGGER.error("Could not capture image, %s", ex) + + async def async_added_to_hass(self) -> None: + """Entity added to Home Assistant.""" + await super().async_added_to_hass() + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image) From 5f2a666e7654ec0b27301d3385b22ba3463213f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Apr 2021 01:32:14 -1000 Subject: [PATCH 032/120] Abort discovery for unsupported doorbird accessories (#48710) --- homeassistant/components/doorbird/__init__.py | 9 +- .../components/doorbird/config_flow.py | 47 ++++--- tests/components/doorbird/test_config_flow.py | 117 +++++++++++++----- 3 files changed, 117 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 8376d75ccbb..3e8e59df203 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,11 +1,10 @@ """Support for DoorBird devices.""" import asyncio import logging -import urllib -from urllib.error import HTTPError from aiohttp import web from doorbirdpy import DoorBird +import requests import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -130,8 +129,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): device = DoorBird(device_ip, username, password) try: status, info = await hass.async_add_executor_job(_init_doorbird_device, device) - except urllib.error.HTTPError as err: - if err.code == HTTP_UNAUTHORIZED: + except requests.exceptions.HTTPError as err: + if err.response.status_code == HTTP_UNAUTHORIZED: _LOGGER.error( "Authorization rejected by DoorBird for %s@%s", username, device_ip ) @@ -202,7 +201,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def _async_register_events(hass, doorstation): try: await hass.async_add_executor_job(doorstation.register_events, hass) - except HTTPError: + except requests.exceptions.HTTPError: hass.components.persistent_notification.async_create( "Doorbird configuration failed. Please verify that API " "Operator permission is enabled for the Doorbird user. " diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 1b39bb4a8c3..f69b38c7a7a 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -1,9 +1,9 @@ """Config flow for DoorBird integration.""" from ipaddress import ip_address import logging -import urllib from doorbirdpy import DoorBird +import requests import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -34,17 +34,18 @@ def _schema_with_defaults(host=None, name=None): ) -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect. +def _check_device(device): + """Verify we can connect to the device and return the status.""" + return device.ready(), device.info() - Data has the keys from DATA_SCHEMA with values provided by the user. - """ + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) try: - status = await hass.async_add_executor_job(device.ready) - info = await hass.async_add_executor_job(device.info) - except urllib.error.HTTPError as err: - if err.code == HTTP_UNAUTHORIZED: + status, info = await hass.async_add_executor_job(_check_device, device) + except requests.exceptions.HTTPError as err: + if err.response.status_code == HTTP_UNAUTHORIZED: raise InvalidAuth from err raise CannotConnect from err except OSError as err: @@ -59,6 +60,19 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": data[CONF_HOST], "mac_addr": mac_addr} +async def async_verify_supported_device(hass, host): + """Verify the doorbell state endpoint returns a 401.""" + device = DoorBird(host, "", "") + try: + await hass.async_add_executor_job(device.doorbell_state) + except requests.exceptions.HTTPError as err: + if err.response.status_code == HTTP_UNAUTHORIZED: + return True + except OSError: + return False + return False + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for DoorBird.""" @@ -85,17 +99,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, discovery_info): """Prepare configuration for a discovered doorbird device.""" macaddress = discovery_info["properties"]["macaddress"] + host = discovery_info[CONF_HOST] if macaddress[:6] != DOORBIRD_OUI: return self.async_abort(reason="not_doorbird_device") - if is_link_local(ip_address(discovery_info[CONF_HOST])): + if is_link_local(ip_address(host)): return self.async_abort(reason="link_local_address") + if not await async_verify_supported_device(self.hass, host): + return self.async_abort(reason="not_doorbird_device") await self.async_set_unique_id(macaddress) - self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info[CONF_HOST]} - ) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) chop_ending = "._axis-video._tcp.local." friendly_hostname = discovery_info["name"] @@ -104,11 +119,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = { CONF_NAME: friendly_hostname, - CONF_HOST: discovery_info[CONF_HOST], + CONF_HOST: host, } - self.discovery_schema = _schema_with_defaults( - host=discovery_info[CONF_HOST], name=friendly_hostname - ) + self.discovery_schema = _schema_with_defaults(host=host, name=friendly_hostname) return await self.async_step_user() diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index e94f73239f1..d6bbb7412e6 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -1,6 +1,8 @@ """Test the DoorBird config flow.""" -from unittest.mock import MagicMock, patch -import urllib +from unittest.mock import MagicMock, Mock, patch + +import pytest +import requests from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN @@ -21,7 +23,9 @@ def _get_mock_doorbirdapi_return_values(ready=None, info=None): doorbirdapi_mock = MagicMock() type(doorbirdapi_mock).ready = MagicMock(return_value=ready) type(doorbirdapi_mock).info = MagicMock(return_value=info) - + type(doorbirdapi_mock).doorbell_state = MagicMock( + side_effect=requests.exceptions.HTTPError(response=Mock(status_code=401)) + ) return doorbirdapi_mock @@ -137,17 +141,25 @@ async def test_form_import_with_zeroconf_already_discovered(hass): await setup.async_setup_component(hass, "persistent_notification", {}) + doorbirdapi = _get_mock_doorbirdapi_return_values( + ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"} + ) # Running the zeroconf init will make the unique id # in progress - zero_conf = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "1CCAE3DOORBIRD"}, - "name": "Doorstation - abc123._axis-video._tcp.local.", - "host": "192.168.1.5", - }, - ) + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ): + zero_conf = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "properties": {"macaddress": "1CCAE3DOORBIRD"}, + "name": "Doorstation - abc123._axis-video._tcp.local.", + "host": "192.168.1.5", + }, + ) + await hass.async_block_till_done() assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM assert zero_conf["step_id"] == "user" assert zero_conf["errors"] == {} @@ -159,9 +171,6 @@ async def test_form_import_with_zeroconf_already_discovered(hass): CONF_CUSTOM_URL ] = "http://legacy.custom.url/should/only/come/in/from/yaml" - doorbirdapi = _get_mock_doorbirdapi_return_values( - ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"} - ) with patch( "homeassistant.components.doorbird.config_flow.DoorBird", return_value=doorbirdapi, @@ -244,24 +253,29 @@ async def test_form_zeroconf_correct_oui(hass): await hass.async_add_executor_job( init_recorder_component, hass ) # force in memory db - - await setup.async_setup_component(hass, "persistent_notification", {}) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "1CCAE3DOORBIRD"}, - "name": "Doorstation - abc123._axis-video._tcp.local.", - "host": "192.168.1.5", - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"] == {} doorbirdapi = _get_mock_doorbirdapi_return_values( ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} ) + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "properties": {"macaddress": "1CCAE3DOORBIRD"}, + "name": "Doorstation - abc123._axis-video._tcp.local.", + "host": "192.168.1.5", + }, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + with patch( "homeassistant.components.doorbird.config_flow.DoorBird", return_value=doorbirdapi, @@ -288,6 +302,43 @@ async def test_form_zeroconf_correct_oui(hass): assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.parametrize( + "doorbell_state_side_effect", + [ + requests.exceptions.HTTPError(response=Mock(status_code=404)), + OSError, + None, + ], +) +async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_effect): + """Test we can setup from zeroconf with the correct OUI source but not a doorstation.""" + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db + doorbirdapi = _get_mock_doorbirdapi_return_values( + ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} + ) + type(doorbirdapi).doorbell_state = MagicMock(side_effect=doorbell_state_side_effect) + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "properties": {"macaddress": "1CCAE3DOORBIRD"}, + "name": "Doorstation - abc123._axis-video._tcp.local.", + "host": "192.168.1.5", + }, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_doorbird_device" + + async def test_form_user_cannot_connect(hass): """Test we handle cannot connect error.""" await hass.async_add_executor_job( @@ -322,10 +373,8 @@ async def test_form_user_invalid_auth(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_urllib_error = urllib.error.HTTPError( - "http://xyz.tld", 401, "login failed", {}, None - ) - doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_urllib_error) + mock_error = requests.exceptions.HTTPError(response=Mock(status_code=401)) + doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_error) with patch( "homeassistant.components.doorbird.config_flow.DoorBird", return_value=doorbirdapi, From 0df9a8ec38339c38992fd9c5c2b081926e7ae1e7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Apr 2021 21:11:42 +0200 Subject: [PATCH 033/120] Improve warnings on undefined template errors (#48713) --- homeassistant/helpers/template.py | 66 +++++++++++++++++++++++++++---- tests/helpers/test_template.py | 5 ++- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 4989c4172ae..9580da82d65 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -6,6 +6,7 @@ import asyncio import base64 import collections.abc from contextlib import suppress +from contextvars import ContextVar from datetime import datetime, timedelta from functools import partial, wraps import json @@ -79,6 +80,8 @@ _COLLECTABLE_STATE_ATTRIBUTES = { ALL_STATES_RATE_LIMIT = timedelta(minutes=1) DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1) +template_cv: ContextVar[str | None] = ContextVar("template_cv", default=None) + @bind_hass def attach(hass: HomeAssistant, obj: Any) -> None: @@ -299,7 +302,7 @@ class Template: self.template: str = template.strip() self._compiled_code = None - self._compiled: Template | None = None + self._compiled: jinja2.Template | None = None self.hass = hass self.is_static = not is_template_string(template) self._limited = None @@ -370,7 +373,7 @@ class Template: kwargs.update(variables) try: - render_result = compiled.render(kwargs) + render_result = _render_with_context(self.template, compiled, **kwargs) except Exception as err: raise TemplateError(err) from err @@ -442,7 +445,7 @@ class Template: def _render_template() -> None: try: - compiled.render(kwargs) + _render_with_context(self.template, compiled, **kwargs) except TimeoutError: pass finally: @@ -524,7 +527,9 @@ class Template: variables["value_json"] = json.loads(value) try: - return self._compiled.render(variables).strip() + return _render_with_context( + self.template, self._compiled, **variables + ).strip() except jinja2.TemplateError as ex: if error_value is _SENTINEL: _LOGGER.error( @@ -535,7 +540,7 @@ class Template: ) return value if error_value is _SENTINEL else error_value - def _ensure_compiled(self, limited: bool = False) -> Template: + def _ensure_compiled(self, limited: bool = False) -> jinja2.Template: """Bind a template to a specific hass instance.""" self.ensure_valid() @@ -548,7 +553,7 @@ class Template: env = self._env self._compiled = cast( - Template, + jinja2.Template, jinja2.Template.from_code(env, self._compiled_code, env.globals, None), ) @@ -1314,12 +1319,59 @@ def urlencode(value): return urllib_urlencode(value).encode("utf-8") +def _render_with_context( + template_str: str, template: jinja2.Template, **kwargs: Any +) -> str: + """Store template being rendered in a ContextVar to aid error handling.""" + template_cv.set(template_str) + return template.render(**kwargs) + + +class LoggingUndefined(jinja2.Undefined): + """Log on undefined variables.""" + + def _log_message(self): + template = template_cv.get() or "" + _LOGGER.warning( + "Template variable warning: %s when rendering '%s'", + self._undefined_message, + template, + ) + + def _fail_with_undefined_error(self, *args, **kwargs): + try: + return super()._fail_with_undefined_error(*args, **kwargs) + except self._undefined_exception as ex: + template = template_cv.get() or "" + _LOGGER.error( + "Template variable error: %s when rendering '%s'", + self._undefined_message, + template, + ) + raise ex + + def __str__(self): + """Log undefined __str___.""" + self._log_message() + return super().__str__() + + def __iter__(self): + """Log undefined __iter___.""" + self._log_message() + return super().__iter__() + + def __bool__(self): + """Log undefined __bool___.""" + self._log_message() + return super().__bool__() + + class TemplateEnvironment(ImmutableSandboxedEnvironment): """The Home Assistant template environment.""" def __init__(self, hass, limited=False): """Initialise template environment.""" - super().__init__(undefined=jinja2.make_logging_undefined(logger=_LOGGER)) + super().__init__(undefined=LoggingUndefined) self.hass = hass self.template_cache = weakref.WeakValueDictionary() self.filters["round"] = forgiving_round diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index da6a8663cc3..a8924f513c6 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2503,4 +2503,7 @@ async def test_undefined_variable(hass, caplog): """Test a warning is logged on undefined variables.""" tpl = template.Template("{{ no_such_variable }}", hass) assert tpl.async_render() == "" - assert "Template variable warning: no_such_variable is undefined" in caplog.text + assert ( + "Template variable warning: 'no_such_variable' is undefined when rendering '{{ no_such_variable }}'" + in caplog.text + ) From 7ae65832eba1b57356f46aee71763930f7d19765 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Apr 2021 09:55:47 +0200 Subject: [PATCH 034/120] Bump pychromecast to 9.1.2 (#48714) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 0c9d0dfc4a5..3f30bc450fd 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.1.1"], + "requirements": ["pychromecast==9.1.2"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 72632ce227d..9bfa679a4eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.1.1 +pychromecast==9.1.2 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07f10ebccd6..8ea503efe96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ pybotvac==0.0.20 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==9.1.1 +pychromecast==9.1.2 # homeassistant.components.climacell pyclimacell==0.14.0 From a7523777ba61246e20bdc1ad9065415cf9ea9f13 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 6 Apr 2021 12:39:29 +0200 Subject: [PATCH 035/120] Flag brightness support for MQTT RGB lights (#48718) --- .../components/mqtt/light/schema_json.py | 4 ++- .../components/mqtt/light/schema_template.py | 2 +- tests/components/mqtt/test_light.py | 29 ++++++++++++++++- tests/components/mqtt/test_light_json.py | 27 ++++++++++++++++ tests/components/mqtt/test_light_template.py | 31 +++++++++++++++++++ 5 files changed, 90 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 4d56435643a..8be3708bd61 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -197,7 +197,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._supported_features |= config[CONF_BRIGHTNESS] and SUPPORT_BRIGHTNESS self._supported_features |= config[CONF_COLOR_TEMP] and SUPPORT_COLOR_TEMP self._supported_features |= config[CONF_HS] and SUPPORT_COLOR - self._supported_features |= config[CONF_RGB] and SUPPORT_COLOR + self._supported_features |= config[CONF_RGB] and ( + SUPPORT_COLOR | SUPPORT_BRIGHTNESS + ) self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE self._supported_features |= config[CONF_XY] and SUPPORT_COLOR diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 118746f2229..7c0266265db 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -417,7 +417,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): and self._templates[CONF_GREEN_TEMPLATE] is not None and self._templates[CONF_BLUE_TEMPLATE] is not None ): - features = features | SUPPORT_COLOR + features = features | SUPPORT_COLOR | SUPPORT_BRIGHTNESS if self._config.get(CONF_EFFECT_LIST) is not None: features = features | SUPPORT_EFFECT if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 00ff8b28b77..e995b373d03 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -161,7 +161,13 @@ import pytest from homeassistant import config as hass_config from homeassistant.components import light -from homeassistant.const import ATTR_ASSUMED_STATE, SERVICE_RELOAD, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_SUPPORTED_FEATURES, + SERVICE_RELOAD, + STATE_OFF, + STATE_ON, +) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -206,6 +212,27 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): assert hass.states.get("light.test") is None +async def test_rgb_light(hass, mqtt_mock): + """Test RGB light flags brightness support.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgb_command_topic": "test_light_rgb/rgb/set", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + expected_features = light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + + async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock): """Test if there is no color and brightness if no topic.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 7834e1d2678..7856eb84c07 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -188,6 +188,33 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): assert hass.states.get("light.test") is None +async def test_rgb_light(hass, mqtt_mock): + """Test RGB light flags brightness support.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgb": True, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + expected_features = ( + light.SUPPORT_TRANSITION + | light.SUPPORT_COLOR + | light.SUPPORT_FLASH + | light.SUPPORT_BRIGHTNESS + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + + async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_mock): """Test for no RGB, brightness, color temp, effect, white val or XY.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 3bbf14ca668..2e726d40ef1 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -141,6 +141,37 @@ async def test_setup_fails(hass, mqtt_mock): assert hass.states.get("light.test") is None +async def test_rgb_light(hass, mqtt_mock): + """Test RGB light flags brightness support.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_light_rgb/set", + "command_on_template": "on", + "command_off_template": "off", + "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + expected_features = ( + light.SUPPORT_TRANSITION + | light.SUPPORT_COLOR + | light.SUPPORT_FLASH + | light.SUPPORT_BRIGHTNESS + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + + async def test_state_change_via_topic(hass, mqtt_mock): """Test state change via topic.""" with assert_setup_component(1, light.DOMAIN): From 315e910bfe2d3ecdcf43bb9f2cb769e6de8d060f Mon Sep 17 00:00:00 2001 From: Justin Paupore Date: Tue, 6 Apr 2021 11:39:54 -0700 Subject: [PATCH 036/120] Fix infinite recursion in LazyState (#48719) If LazyState cannot parse the attributes of its row as JSON, it prints a message to the logger. Unfortunately, it passes `self` as a format argument to that message, which causes its `__repr__` method to be called, which then tries to retrieve `self.attributes` in order to display them. This leads to an infinite recursion and a crash of the entire core. To fix, send the database row to be printed in the log message, rather than the LazyState object that wraps around it. --- homeassistant/components/history/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 59fdcc7811b..09f459b32d6 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -715,7 +715,7 @@ class LazyState(State): self._attributes = json.loads(self._row.attributes) except ValueError: # When json.loads fails - _LOGGER.exception("Error converting row to state: %s", self) + _LOGGER.exception("Error converting row to state: %s", self._row) self._attributes = {} return self._attributes From d61780dbac6393f440a1a42302e89f0a9288a29a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Apr 2021 12:10:39 -0700 Subject: [PATCH 037/120] Allow reloading top-level template entities (#48733) --- homeassistant/components/template/__init__.py | 91 ++++++++++++++++--- homeassistant/components/template/config.py | 68 +++++++------- tests/components/template/test_init.py | 40 ++++++-- .../template/sensor_configuration.yaml | 7 ++ 4 files changed, 152 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index f9b6b3b4975..72a97d6eeab 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1,54 +1,112 @@ """The template component.""" -import logging -from typing import Optional +from __future__ import annotations +import asyncio +import logging +from typing import Callable + +from homeassistant import config as conf_util from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import CoreState, callback +from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD +from homeassistant.core import CoreState, Event, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( discovery, trigger as trigger_helper, update_coordinator, ) -from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.loader import async_get_integration from .const import CONF_TRIGGER, DOMAIN, PLATFORMS +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass, config): """Set up the template integration.""" if DOMAIN in config: - for conf in config[DOMAIN]: - coordinator = TriggerUpdateCoordinator(hass, conf) - await coordinator.async_setup(config) + await _process_config(hass, config) - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + async def _reload_config(call: Event) -> None: + """Reload top-level + platforms.""" + try: + unprocessed_conf = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + conf = await conf_util.async_process_component_config( + hass, unprocessed_conf, await async_get_integration(hass, DOMAIN) + ) + + if conf is None: + return + + await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) + + if DOMAIN in conf: + await _process_config(hass, conf) + + hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) + + hass.helpers.service.async_register_admin_service( + DOMAIN, SERVICE_RELOAD, _reload_config + ) return True +async def _process_config(hass, config): + """Process config.""" + coordinators: list[TriggerUpdateCoordinator] | None = hass.data.get(DOMAIN) + + # Remove old ones + if coordinators: + for coordinator in coordinators: + coordinator.async_remove() + + async def init_coordinator(hass, conf): + coordinator = TriggerUpdateCoordinator(hass, conf) + await coordinator.async_setup(conf) + return coordinator + + hass.data[DOMAIN] = await asyncio.gather( + *[init_coordinator(hass, conf) for conf in config[DOMAIN]] + ) + + class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): """Class to handle incoming data.""" + REMOVE_TRIGGER = object() + def __init__(self, hass, config): """Instantiate trigger data.""" - super().__init__( - hass, logging.getLogger(__name__), name="Trigger Update Coordinator" - ) + super().__init__(hass, _LOGGER, name="Trigger Update Coordinator") self.config = config - self._unsub_trigger = None + self._unsub_start: Callable[[], None] | None = None + self._unsub_trigger: Callable[[], None] | None = None @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return unique ID for the entity.""" return self.config.get("unique_id") + @callback + def async_remove(self): + """Signal that the entities need to remove themselves.""" + if self._unsub_start: + self._unsub_start() + if self._unsub_trigger: + self._unsub_trigger() + async def async_setup(self, hass_config): """Set up the trigger and create entities.""" if self.hass.state == CoreState.running: await self._attach_triggers() else: - self.hass.bus.async_listen_once( + self._unsub_start = self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, self._attach_triggers ) @@ -65,6 +123,9 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): async def _attach_triggers(self, start_event=None) -> None: """Attach the triggers.""" + if start_event is not None: + self._unsub_start = None + self._unsub_trigger = await trigger_helper.async_initialize_triggers( self.hass, self.config[CONF_TRIGGER], diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index edef5673f31..5d1a66836f3 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -36,7 +36,7 @@ from .const import ( ) from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA -CONVERSION_PLATFORM = { +LEGACY_SENSOR = { CONF_ICON_TEMPLATE: CONF_ICON, CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, @@ -61,7 +61,7 @@ SENSOR_SCHEMA = vol.Schema( } ) -TRIGGER_ENTITY_SCHEMA = vol.Schema( +CONFIG_SECTION_SCHEMA = vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, @@ -71,16 +71,43 @@ TRIGGER_ENTITY_SCHEMA = vol.Schema( ) +def _rewrite_legacy_to_modern_trigger_conf(cfg: dict): + """Rewrite a legacy to a modern trigger-basd conf.""" + logging.getLogger(__name__).warning( + "The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" + ) + sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] + + for device_id, entity_cfg in cfg[CONF_SENSORS].items(): + entity_cfg = {**entity_cfg} + + for from_key, to_key in LEGACY_SENSOR.items(): + if from_key not in entity_cfg or to_key in entity_cfg: + continue + + val = entity_cfg.pop(from_key) + if isinstance(val, str): + val = template.Template(val) + entity_cfg[to_key] = val + + if CONF_NAME not in entity_cfg: + entity_cfg[CONF_NAME] = template.Template(device_id) + + sensor.append(entity_cfg) + + return {**cfg, "sensor": sensor} + + async def async_validate_config(hass, config): """Validate config.""" if DOMAIN not in config: return config - trigger_entity_configs = [] + config_sections = [] for cfg in cv.ensure_list(config[DOMAIN]): try: - cfg = TRIGGER_ENTITY_SCHEMA(cfg) + cfg = CONFIG_SECTION_SCHEMA(cfg) cfg[CONF_TRIGGER] = await async_validate_trigger_config( hass, cfg[CONF_TRIGGER] ) @@ -88,39 +115,14 @@ async def async_validate_config(hass, config): async_log_exception(err, DOMAIN, cfg, hass) continue - if CONF_SENSORS not in cfg: - trigger_entity_configs.append(cfg) - continue + if CONF_TRIGGER in cfg and CONF_SENSORS in cfg: + cfg = _rewrite_legacy_to_modern_trigger_conf(cfg) - logging.getLogger(__name__).warning( - "The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" - ) - sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] - - for device_id, entity_cfg in cfg[CONF_SENSORS].items(): - entity_cfg = {**entity_cfg} - - for from_key, to_key in CONVERSION_PLATFORM.items(): - if from_key not in entity_cfg or to_key in entity_cfg: - continue - - val = entity_cfg.pop(from_key) - if isinstance(val, str): - val = template.Template(val) - entity_cfg[to_key] = val - - if CONF_NAME not in entity_cfg: - entity_cfg[CONF_NAME] = template.Template(device_id) - - sensor.append(entity_cfg) - - cfg = {**cfg, "sensor": sensor} - - trigger_entity_configs.append(cfg) + config_sections.append(cfg) # Create a copy of the configuration with all config for current # component removed and add validated config back in. config = config_without_domain(config, DOMAIN) - config[DOMAIN] = trigger_entity_configs + config[DOMAIN] = config_sections return config diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 107c54c710e..0f8dff4026f 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -27,7 +27,14 @@ async def test_reloadable(hass): "value_template": "{{ states.sensor.test_sensor.state }}" }, }, - } + }, + "template": { + "trigger": {"platform": "event", "event_type": "event_1"}, + "sensor": { + "name": "top level", + "state": "{{ trigger.event.data.source }}", + }, + }, }, ) await hass.async_block_till_done() @@ -35,8 +42,12 @@ async def test_reloadable(hass): await hass.async_start() await hass.async_block_till_done() + hass.bus.async_fire("event_1", {"source": "init"}) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 assert hass.states.get("sensor.state").state == "mytest" - assert len(hass.states.async_all()) == 2 + assert hass.states.get("sensor.top_level").state == "init" yaml_path = path.join( _get_fixtures_base_path(), @@ -52,11 +63,16 @@ async def test_reloadable(hass): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 + + hass.bus.async_fire("event_2", {"source": "reload"}) + await hass.async_block_till_done() assert hass.states.get("sensor.state") is None + assert hass.states.get("sensor.top_level") is None assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off" assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0 + assert hass.states.get("sensor.top_level_2").state == "reload" async def test_reloadable_can_remove(hass): @@ -74,7 +90,14 @@ async def test_reloadable_can_remove(hass): "value_template": "{{ states.sensor.test_sensor.state }}" }, }, - } + }, + "template": { + "trigger": {"platform": "event", "event_type": "event_1"}, + "sensor": { + "name": "top level", + "state": "{{ trigger.event.data.source }}", + }, + }, }, ) await hass.async_block_till_done() @@ -82,8 +105,12 @@ async def test_reloadable_can_remove(hass): await hass.async_start() await hass.async_block_till_done() + hass.bus.async_fire("event_1", {"source": "init"}) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 assert hass.states.get("sensor.state").state == "mytest" - assert len(hass.states.async_all()) == 2 + assert hass.states.get("sensor.top_level").state == "init" yaml_path = path.join( _get_fixtures_base_path(), @@ -251,11 +278,12 @@ async def test_reloadable_multiple_platforms(hass): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 assert hass.states.get("sensor.state") is None assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off" assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0 + assert hass.states.get("sensor.top_level_2") is not None async def test_reload_sensors_that_reference_other_template_sensors(hass): diff --git a/tests/fixtures/template/sensor_configuration.yaml b/tests/fixtures/template/sensor_configuration.yaml index 48ef4cf4304..8fb2ae9564f 100644 --- a/tests/fixtures/template/sensor_configuration.yaml +++ b/tests/fixtures/template/sensor_configuration.yaml @@ -21,3 +21,10 @@ sensor: == "Watch TV" or state_attr("remote.alexander_master_bedroom","current_activity") == "Watch Apple TV" %}on{% else %}off{% endif %}' +template: + trigger: + platform: event + event_type: event_2 + sensor: + name: top level 2 + state: "{{ trigger.event.data.source }}" From ed90e224213060261d9ecc483fbbce29e64aabff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Apr 2021 11:14:54 -0700 Subject: [PATCH 038/120] Updated frontend to 20210406.0 (#48734) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 55392323f3d..b659ec7e7d4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210402.1" + "home-assistant-frontend==20210406.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 64801ad1afb..4296705c54f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210402.1 +home-assistant-frontend==20210406.0 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 9bfa679a4eb..a39bc00d061 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210402.1 +home-assistant-frontend==20210406.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ea503efe96..5f1ac7757ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210402.1 +home-assistant-frontend==20210406.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 2db60a3c564c51562c702d5e97b3b7ee3901f058 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Apr 2021 19:12:33 +0000 Subject: [PATCH 039/120] Bumped version to 2021.4.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f1bedbb5e6a..2d772e9fa3c 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 = "0b4" +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 15e00b8d18a93ef9d7e803ac47817a3d592fdecd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Apr 2021 09:39:39 +0200 Subject: [PATCH 040/120] Do not activate Met.no without setting a Home coordinates (#48741) --- homeassistant/components/met/__init__.py | 21 ++++++++++++- homeassistant/components/met/config_flow.py | 16 +++++++++- homeassistant/components/met/const.py | 3 ++ homeassistant/components/met/strings.json | 7 ++++- tests/components/met/__init__.py | 12 +++++--- tests/components/met/test_config_flow.py | 20 ++++++++++++ tests/components/met/test_init.py | 34 +++++++++++++++++++-- 7 files changed, 104 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 4367ca98536..f1f33b83490 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -19,7 +19,12 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from homeassistant.util.distance import convert as convert_distance import homeassistant.util.dt as dt_util -from .const import CONF_TRACK_HOME, DOMAIN +from .const import ( + CONF_TRACK_HOME, + DEFAULT_HOME_LATITUDE, + DEFAULT_HOME_LONGITUDE, + DOMAIN, +) URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" @@ -35,6 +40,20 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: async def async_setup_entry(hass, config_entry): """Set up Met as config entry.""" + # Don't setup if tracking home location and latitude or longitude isn't set. + # Also, filters out our onboarding default location. + if config_entry.data.get(CONF_TRACK_HOME, False) and ( + (not hass.config.latitude and not hass.config.longitude) + or ( + hass.config.latitude == DEFAULT_HOME_LATITUDE + and hass.config.longitude == DEFAULT_HOME_LONGITUDE + ) + ): + _LOGGER.warning( + "Skip setting up met.no integration; No Home location has been set" + ) + return False + coordinator = MetDataUpdateCoordinator(hass, config_entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index b9d50ba59a5..5cfd71ea801 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -10,7 +10,13 @@ from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, C from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME +from .const import ( + CONF_TRACK_HOME, + DEFAULT_HOME_LATITUDE, + DEFAULT_HOME_LONGITUDE, + DOMAIN, + HOME_LOCATION_NAME, +) @callback @@ -81,6 +87,14 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_onboarding(self, data=None): """Handle a flow initialized by onboarding.""" + # Don't create entry if latitude or longitude isn't set. + # Also, filters out our onboarding default location. + if (not self.hass.config.latitude and not self.hass.config.longitude) or ( + self.hass.config.latitude == DEFAULT_HOME_LATITUDE + and self.hass.config.longitude == DEFAULT_HOME_LONGITUDE + ): + return self.async_abort(reason="no_home") + return self.async_create_entry( title=HOME_LOCATION_NAME, data={CONF_TRACK_HOME: True} ) diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index b78c412393d..0f4c22dbba3 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -34,6 +34,9 @@ HOME_LOCATION_NAME = "Home" CONF_TRACK_HOME = "track_home" +DEFAULT_HOME_LATITUDE = 52.3731339 +DEFAULT_HOME_LONGITUDE = 4.8903147 + ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_{HOME_LOCATION_NAME}" CONDITIONS_MAP = { diff --git a/homeassistant/components/met/strings.json b/homeassistant/components/met/strings.json index b9e94aba865..b9d251e21d8 100644 --- a/homeassistant/components/met/strings.json +++ b/homeassistant/components/met/strings.json @@ -12,6 +12,11 @@ } } }, - "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "abort": { + "no_home": "No home coordinates are set in the Home Assistant configuration" + } } } diff --git a/tests/components/met/__init__.py b/tests/components/met/__init__.py index 13b186f3b47..0a17b415965 100644 --- a/tests/components/met/__init__.py +++ b/tests/components/met/__init__.py @@ -1,20 +1,24 @@ """Tests for Met.no.""" from unittest.mock import patch -from homeassistant.components.met.const import DOMAIN +from homeassistant.components.met.const import CONF_TRACK_HOME, DOMAIN from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from tests.common import MockConfigEntry -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass, track_home=False) -> MockConfigEntry: """Set up the Met integration in Home Assistant.""" entry_data = { CONF_NAME: "test", CONF_LATITUDE: 0, - CONF_LONGITUDE: 0, - CONF_ELEVATION: 0, + CONF_LONGITUDE: 1.0, + CONF_ELEVATION: 1.0, } + + if track_home: + entry_data = {CONF_TRACK_HOME: True} + entry = MockConfigEntry(domain=DOMAIN, data=entry_data) with patch( "homeassistant.components.met.metno.MetWeatherData.fetching_data", diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 622475e8376..25e123f67e8 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME +from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE from tests.common import MockConfigEntry @@ -106,6 +107,25 @@ async def test_onboarding_step(hass): assert result["data"] == {"track_home": True} +@pytest.mark.parametrize("latitude,longitude", [(52.3731339, 4.8903147), (0.0, 0.0)]) +async def test_onboarding_step_abort_no_home(hass, latitude, longitude): + """Test entry not created when default step fails.""" + await async_process_ha_core_config( + hass, + {"latitude": latitude, "longitude": longitude}, + ) + + assert hass.config.latitude == latitude + assert hass.config.longitude == longitude + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "onboarding"}, data={} + ) + + assert result["type"] == "abort" + assert result["reason"] == "no_home" + + async def test_import_step(hass): """Test initializing via import step.""" test_data = { diff --git a/tests/components/met/test_init.py b/tests/components/met/test_init.py index a3323f01565..074293249c8 100644 --- a/tests/components/met/test_init.py +++ b/tests/components/met/test_init.py @@ -1,6 +1,15 @@ """Test the Met integration init.""" -from homeassistant.components.met.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.components.met.const import ( + DEFAULT_HOME_LATITUDE, + DEFAULT_HOME_LONGITUDE, + DOMAIN, +) +from homeassistant.config import async_process_ha_core_config +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_ERROR, +) from . import init_integration @@ -17,3 +26,24 @@ async def test_unload_entry(hass): assert entry.state == ENTRY_STATE_NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_fail_default_home_entry(hass, caplog): + """Test abort setup of default home location.""" + await async_process_ha_core_config( + hass, + {"latitude": 52.3731339, "longitude": 4.8903147}, + ) + + assert hass.config.latitude == DEFAULT_HOME_LATITUDE + assert hass.config.longitude == DEFAULT_HOME_LONGITUDE + + entry = await init_integration(hass, track_home=True) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_SETUP_ERROR + + assert ( + "Skip setting up met.no integration; No Home location has been set" + in caplog.text + ) From b573fb49b751f5107f5f03c5ea8693424e6ae1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 7 Apr 2021 02:34:49 +0200 Subject: [PATCH 041/120] Generate a seperate UUID for the analytics integration (#48742) --- .../components/analytics/__init__.py | 5 +- .../components/analytics/analytics.py | 18 +++-- homeassistant/components/analytics/const.py | 2 +- tests/components/analytics/test_analytics.py | 67 ++++++++++++------- tests/components/analytics/test_init.py | 10 +-- 5 files changed, 60 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index 3a06c56add5..c1187af7f17 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_call_later, async_track_time_interval from .analytics import Analytics -from .const import ATTR_HUUID, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA +from .const import ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA async def async_setup(hass: HomeAssistant, _): @@ -44,10 +44,9 @@ async def websocket_analytics( ) -> None: """Return analytics preferences.""" analytics: Analytics = hass.data[DOMAIN] - huuid = await hass.helpers.instance_id.async_get() connection.send_result( msg["id"], - {ATTR_PREFERENCES: analytics.preferences, ATTR_HUUID: huuid}, + {ATTR_PREFERENCES: analytics.preferences}, ) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index ef7c2fbde6e..d7764a052c8 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -1,5 +1,6 @@ """Analytics helper class for the analytics integration.""" import asyncio +import uuid import aiohttp import async_timeout @@ -24,7 +25,6 @@ from .const import ( ATTR_BASE, ATTR_DIAGNOSTICS, ATTR_HEALTHY, - ATTR_HUUID, ATTR_INTEGRATION_COUNT, ATTR_INTEGRATIONS, ATTR_ONBOARDED, @@ -37,6 +37,7 @@ from .const import ( ATTR_SUPPORTED, ATTR_USAGE, ATTR_USER_COUNT, + ATTR_UUID, ATTR_VERSION, LOGGER, PREFERENCE_SCHEMA, @@ -52,7 +53,7 @@ class Analytics: """Initialize the Analytics class.""" self.hass: HomeAssistant = hass self.session = async_get_clientsession(hass) - self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False} + self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None} self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @property @@ -71,6 +72,11 @@ class Analytics: """Return bool if the user has made a choice.""" return self._data[ATTR_ONBOARDED] + @property + def uuid(self) -> bool: + """Return the uuid for the analytics integration.""" + return self._data[ATTR_UUID] + @property def supervisor(self) -> bool: """Return bool if a supervisor is present.""" @@ -81,6 +87,7 @@ class Analytics: stored = await self._store.async_load() if stored: self._data = stored + if self.supervisor: supervisor_info = hassio.get_supervisor_info(self.hass) if not self.onboarded: @@ -99,6 +106,7 @@ class Analytics: preferences = PREFERENCE_SCHEMA(preferences) self._data[ATTR_PREFERENCES].update(preferences) self._data[ATTR_ONBOARDED] = True + await self._store.async_save(self._data) if self.supervisor: @@ -114,7 +122,9 @@ class Analytics: LOGGER.debug("Nothing to submit") return - huuid = await self.hass.helpers.instance_id.async_get() + if self._data.get(ATTR_UUID) is None: + self._data[ATTR_UUID] = uuid.uuid4().hex + await self._store.async_save(self._data) if self.supervisor: supervisor_info = hassio.get_supervisor_info(self.hass) @@ -123,7 +133,7 @@ class Analytics: integrations = [] addons = [] payload: dict = { - ATTR_HUUID: huuid, + ATTR_UUID: self.uuid, ATTR_VERSION: HA_VERSION, ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE], } diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index ba56ba265a7..998dac9cf80 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -20,7 +20,6 @@ ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" ATTR_DIAGNOSTICS = "diagnostics" ATTR_HEALTHY = "healthy" -ATTR_HUUID = "huuid" ATTR_INSTALLATION_TYPE = "installation_type" ATTR_INTEGRATION_COUNT = "integration_count" ATTR_INTEGRATIONS = "integrations" @@ -34,6 +33,7 @@ ATTR_SUPERVISOR = "supervisor" ATTR_SUPPORTED = "supported" ATTR_USAGE = "usage" ATTR_USER_COUNT = "user_count" +ATTR_UUID = "uuid" ATTR_VERSION = "version" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 1a636d16598..f7f55c510c1 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1,5 +1,5 @@ """The tests for the analytics .""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock, PropertyMock, patch import aiohttp import pytest @@ -13,10 +13,11 @@ from homeassistant.components.analytics.const import ( ATTR_STATISTICS, ATTR_USAGE, ) +from homeassistant.components.api import ATTR_UUID from homeassistant.const import __version__ as HA_VERSION from homeassistant.loader import IntegrationNotFound -MOCK_HUUID = "abcdefg" +MOCK_UUID = "abcdefg" async def test_no_send(hass, caplog, aioclient_mock): @@ -26,8 +27,7 @@ async def test_no_send(hass, caplog, aioclient_mock): with patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=False), - ), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.load() + ): assert not analytics.preferences[ATTR_BASE] await analytics.send_analytics() @@ -76,9 +76,7 @@ async def test_failed_to_send(hass, caplog, aioclient_mock): analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.send_analytics() + await analytics.send_analytics() assert "Sending analytics failed with statuscode 400" in caplog.text @@ -88,9 +86,7 @@ async def test_failed_to_send_raises(hass, caplog, aioclient_mock): analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.send_analytics() + await analytics.send_analytics() assert "Error sending analytics" in caplog.text @@ -98,12 +94,15 @@ async def test_send_base(hass, caplog, aioclient_mock): """Test send base prefrences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) + await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex: + hex.return_value = MOCK_UUID await analytics.send_analytics() - assert f"'huuid': '{MOCK_HUUID}'" in caplog.text + + assert f"'uuid': '{MOCK_UUID}'" in caplog.text assert f"'version': '{HA_VERSION}'" in caplog.text assert "'installation_type':" in caplog.text assert "'integration_count':" not in caplog.text @@ -131,10 +130,14 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), ), patch( - "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID - ): + "uuid.UUID.hex", new_callable=PropertyMock + ) as hex: + hex.return_value = MOCK_UUID + await analytics.load() + await analytics.send_analytics() - assert f"'huuid': '{MOCK_HUUID}'" in caplog.text + + assert f"'uuid': '{MOCK_UUID}'" in caplog.text assert f"'version': '{HA_VERSION}'" in caplog.text assert "'supervisor': {'healthy': True, 'supported': True}}" in caplog.text assert "'installation_type':" in caplog.text @@ -147,12 +150,13 @@ async def test_send_usage(hass, caplog, aioclient_mock): aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + assert analytics.preferences[ATTR_BASE] assert analytics.preferences[ATTR_USAGE] hass.config.components = ["default_config"] - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.send_analytics() + await analytics.send_analytics() + assert "'integrations': ['default_config']" in caplog.text assert "'integration_count':" not in caplog.text @@ -195,8 +199,6 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): ), patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), - ), patch( - "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID ): await analytics.send_analytics() assert ( @@ -215,8 +217,7 @@ async def test_send_statistics(hass, caplog, aioclient_mock): assert analytics.preferences[ATTR_STATISTICS] hass.config.components = ["default_config"] - with patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): - await analytics.send_analytics() + await analytics.send_analytics() assert ( "'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0" in caplog.text @@ -236,11 +237,11 @@ async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_moc with patch( "homeassistant.components.analytics.analytics.async_get_integration", side_effect=IntegrationNotFound("any"), - ), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + ): await analytics.send_analytics() post_call = aioclient_mock.mock_calls[0] - assert "huuid" in post_call[2] + assert "uuid" in post_call[2] assert post_call[2]["integration_count"] == 0 @@ -258,7 +259,7 @@ async def test_send_statistics_async_get_integration_unknown_exception( with pytest.raises(ValueError), patch( "homeassistant.components.analytics.analytics.async_get_integration", side_effect=ValueError, - ), patch("homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID): + ): await analytics.send_analytics() @@ -298,9 +299,23 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): ), patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), - ), patch( - "homeassistant.helpers.instance_id.async_get", return_value=MOCK_HUUID ): await analytics.send_analytics() assert "'addon_count': 1" in caplog.text assert "'integrations':" not in caplog.text + + +async def test_reusing_uuid(hass, aioclient_mock): + """Test reusing the stored UUID.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + analytics._data[ATTR_UUID] = "NOT_MOCK_UUID" + + await analytics.save_preferences({ATTR_BASE: True}) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex: + # This is not actually called but that in itself prove the test + hex.return_value = MOCK_UUID + await analytics.send_analytics() + + assert analytics.uuid == "NOT_MOCK_UUID" diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index 4f8c95bc6b4..af105926926 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -1,6 +1,4 @@ """The tests for the analytics .""" -from unittest.mock import patch - from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN from homeassistant.setup import async_setup_component @@ -22,11 +20,9 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock): ws_client = await hass_ws_client(hass) await ws_client.send_json({"id": 1, "type": "analytics"}) - with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): - response = await ws_client.receive_json() + response = await ws_client.receive_json() assert response["success"] - assert response["result"]["huuid"] == "abcdef" await ws_client.send_json( {"id": 2, "type": "analytics/preferences", "preferences": {"base": True}} @@ -36,7 +32,5 @@ async def test_websocket(hass, hass_ws_client, aioclient_mock): assert response["result"]["preferences"]["base"] await ws_client.send_json({"id": 3, "type": "analytics"}) - with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): - response = await ws_client.receive_json() + response = await ws_client.receive_json() assert response["result"]["preferences"]["base"] - assert response["result"]["huuid"] == "abcdef" From 322458ee49f81ec8028c9816425867f0440e1031 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Apr 2021 00:46:47 +0200 Subject: [PATCH 042/120] Rename hassio config entry title to Supervisor (#48748) --- homeassistant/components/hassio/config_flow.py | 2 +- tests/components/hassio/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/config_flow.py b/homeassistant/components/hassio/config_flow.py index 8b2c68d752d..acc39f4cf91 100644 --- a/homeassistant/components/hassio/config_flow.py +++ b/homeassistant/components/hassio/config_flow.py @@ -19,4 +19,4 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # We only need one Hass.io config entry await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() - return self.async_create_entry(title=DOMAIN.title(), data={}) + return self.async_create_entry(title="Supervisor", data={}) diff --git a/tests/components/hassio/test_config_flow.py b/tests/components/hassio/test_config_flow.py index c2d306183f0..2b4b8a88914 100644 --- a/tests/components/hassio/test_config_flow.py +++ b/tests/components/hassio/test_config_flow.py @@ -18,7 +18,7 @@ async def test_config_flow(hass): DOMAIN, context={"source": "system"} ) assert result["type"] == "create_entry" - assert result["title"] == DOMAIN.title() + assert result["title"] == "Supervisor" assert result["data"] == {} await hass.async_block_till_done() From a093cd8ac2c51ab9ddf5adcad3d698ffb4f2cbfa Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 7 Apr 2021 09:13:55 +0200 Subject: [PATCH 043/120] Use microsecond precision for datetime values on MariaDB/MySQL (#48749) Co-authored-by: Paulus Schoutsen --- homeassistant/components/recorder/migration.py | 14 ++++++++++++++ homeassistant/components/recorder/models.py | 16 ++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index e730b1af239..5ab2d909172 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -363,6 +363,20 @@ def _apply_update(engine, new_version, old_version): if engine.dialect.name == "mysql": _modify_columns(engine, "events", ["event_data LONGTEXT"]) _modify_columns(engine, "states", ["attributes LONGTEXT"]) + elif new_version == 13: + if engine.dialect.name == "mysql": + _modify_columns( + engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"] + ) + _modify_columns( + engine, + "states", + [ + "last_changed DATETIME(6)", + "last_updated DATETIME(6)", + "created DATETIME(6)", + ], + ) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ef7181c9c03..a547f315133 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -26,7 +26,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 12 +SCHEMA_VERSION = 13 _LOGGER = logging.getLogger(__name__) @@ -39,6 +39,10 @@ TABLE_SCHEMA_CHANGES = "schema_changes" ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES] +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) + class Events(Base): # type: ignore """Event history data.""" @@ -52,8 +56,8 @@ class Events(Base): # type: ignore event_type = Column(String(32)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(32)) - time_fired = Column(DateTime(timezone=True), index=True) - created = Column(DateTime(timezone=True), default=dt_util.utcnow) + time_fired = Column(DATETIME_TYPE, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) context_id = Column(String(36), index=True) context_user_id = Column(String(36), index=True) context_parent_id = Column(String(36), index=True) @@ -123,9 +127,9 @@ class States(Base): # type: ignore event_id = Column( Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True ) - last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow) - last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True) - created = Column(DateTime(timezone=True), default=dt_util.utcnow) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) old_state_id = Column( Integer, ForeignKey("states.state_id", ondelete="NO ACTION"), index=True ) From 537d6412dd61d2402df648e83669625df16ab3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 7 Apr 2021 04:33:08 +0200 Subject: [PATCH 044/120] Add custom integrations to analytics (#48753) --- homeassistant/components/analytics/analytics.py | 16 ++++++++++++++-- homeassistant/components/analytics/const.py | 1 + tests/components/analytics/test_analytics.py | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index d7764a052c8..e6e8678cc10 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -8,7 +8,7 @@ import async_timeout from homeassistant.components import hassio from homeassistant.components.api import ATTR_INSTALLATION_TYPE from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN -from homeassistant.const import __version__ as HA_VERSION +from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.storage import Store @@ -23,6 +23,7 @@ from .const import ( ATTR_AUTO_UPDATE, ATTR_AUTOMATION_COUNT, ATTR_BASE, + ATTR_CUSTOM_INTEGRATIONS, ATTR_DIAGNOSTICS, ATTR_HEALTHY, ATTR_INTEGRATION_COUNT, @@ -131,6 +132,7 @@ class Analytics: system_info = await async_get_system_info(self.hass) integrations = [] + custom_integrations = [] addons = [] payload: dict = { ATTR_UUID: self.uuid, @@ -162,7 +164,16 @@ class Analytics: if isinstance(integration, BaseException): raise integration - if integration.disabled or not integration.is_built_in: + if integration.disabled: + continue + + if not integration.is_built_in: + custom_integrations.append( + { + ATTR_DOMAIN: integration.domain, + ATTR_VERSION: integration.version, + } + ) continue integrations.append(integration.domain) @@ -186,6 +197,7 @@ class Analytics: if self.preferences.get(ATTR_USAGE, False): payload[ATTR_INTEGRATIONS] = integrations + payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations if supervisor_info is not None: payload[ATTR_ADDONS] = addons diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 998dac9cf80..a6fe91b5a44 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -18,6 +18,7 @@ ATTR_ADDONS = "addons" ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" +ATTR_CUSTOM_INTEGRATIONS = "custom_integrations" ATTR_DIAGNOSTICS = "diagnostics" ATTR_HEALTHY = "healthy" ATTR_INSTALLATION_TYPE = "installation_type" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index f7f55c510c1..e1716df9cdb 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -14,8 +14,9 @@ from homeassistant.components.analytics.const import ( ATTR_USAGE, ) from homeassistant.components.api import ATTR_UUID -from homeassistant.const import __version__ as HA_VERSION +from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION from homeassistant.loader import IntegrationNotFound +from homeassistant.setup import async_setup_component MOCK_UUID = "abcdefg" @@ -319,3 +320,16 @@ async def test_reusing_uuid(hass, aioclient_mock): await analytics.send_analytics() assert analytics.uuid == "NOT_MOCK_UUID" + + +async def test_custom_integrations(hass, aioclient_mock): + """Test sending custom integrations.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + assert await async_setup_component(hass, "test_package", {"test_package": {}}) + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + + await analytics.send_analytics() + + payload = aioclient_mock.mock_calls[0][2] + assert payload["custom_integrations"][0][ATTR_DOMAIN] == "test_package" From bfb8141f55297e3ff942c71d4a7733fae5cbad89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Apr 2021 21:39:04 -1000 Subject: [PATCH 045/120] Solve cast delaying startup when discovered devices are slow to setup (#48755) * Solve cast delaying startup when devices are slow to setup * Update homeassistant/components/cast/media_player.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- homeassistant/components/cast/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 7c2a696027f..25b2674821d 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1,6 +1,7 @@ """Provide functionality to interact with Cast devices on the network.""" from __future__ import annotations +import asyncio from contextlib import suppress from datetime import timedelta import functools as ft @@ -185,7 +186,9 @@ class CastDevice(MediaPlayerEntity): ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) self.async_set_cast_info(self._cast_info) - self.hass.async_create_task( + # asyncio.create_task is used to avoid delaying startup wrapup if the device + # is discovered already during startup but then fails to respond + asyncio.create_task( async_create_catching_coro(self.async_connect_to_chromecast()) ) From e366961ddbf244af08cf9c86ec24313f75fd6399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 7 Apr 2021 09:27:58 +0200 Subject: [PATCH 046/120] Met.no - only update data if coordinates changed (#48756) --- homeassistant/components/met/__init__.py | 15 ++++++++++----- tests/components/met/test_weather.py | 5 +++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index f1f33b83490..47d946b92e7 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -87,7 +87,7 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator): self.weather = MetWeatherData( hass, config_entry.data, hass.config.units.is_metric ) - self.weather.init_data() + self.weather.set_coordinates() update_interval = timedelta(minutes=randrange(55, 65)) @@ -107,8 +107,8 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_weather_data(_event=None): """Update weather data.""" - self.weather.init_data() - await self.async_refresh() + if self.weather.set_coordinates(): + await self.async_refresh() self._unsub_track_home = self.hass.bus.async_listen( EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data @@ -133,9 +133,10 @@ class MetWeatherData: self.current_weather_data = {} self.daily_forecast = None self.hourly_forecast = None + self._coordinates = None - def init_data(self): - """Weather data inialization - get the coordinates.""" + def set_coordinates(self): + """Weather data inialization - set the coordinates.""" if self._config.get(CONF_TRACK_HOME, False): latitude = self.hass.config.latitude longitude = self.hass.config.longitude @@ -155,10 +156,14 @@ class MetWeatherData: "lon": str(longitude), "msl": str(elevation), } + if coordinates == self._coordinates: + return False + self._coordinates = coordinates self._weather_data = metno.MetWeatherData( coordinates, async_get_clientsession(self.hass), api_url=URL ) + return True async def fetch_data(self): """Fetch data from API - (current weather and forecast).""" diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 89c1dc62612..92e9b674668 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -29,6 +29,11 @@ async def test_tracking_home(hass, mock_weather): assert len(mock_weather.mock_calls) == 8 + # Same coordinates again should not trigger any new requests to met.no + await hass.config.async_update(latitude=10, longitude=20) + await hass.async_block_till_done() + assert len(mock_weather.mock_calls) == 8 + entry = hass.config_entries.async_entries()[0] await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() From 32511409a91200eccaea1bc2dd2422db6f92cc66 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Apr 2021 12:15:56 +0200 Subject: [PATCH 047/120] Remove login details before logging SQL errors (#48758) --- homeassistant/components/sql/sensor.py | 24 ++++++++++++++-- tests/components/sql/test_sensor.py | 40 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index a537b160d0b..b90ce2f8e59 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -2,6 +2,7 @@ import datetime import decimal import logging +import re import sqlalchemy from sqlalchemy.orm import scoped_session, sessionmaker @@ -18,6 +19,13 @@ CONF_COLUMN_NAME = "column" CONF_QUERIES = "queries" CONF_QUERY = "query" +DB_URL_RE = re.compile("//.*:.*@") + + +def redact_credentials(data): + """Redact credentials from string data.""" + return DB_URL_RE.sub("//****:****@", data) + def validate_sql_select(value): """Validate that value is a SQL SELECT query.""" @@ -47,6 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not db_url: db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + sess = None try: engine = sqlalchemy.create_engine(db_url) sessmaker = scoped_session(sessionmaker(bind=engine)) @@ -56,10 +65,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sess.execute("SELECT 1;") except sqlalchemy.exc.SQLAlchemyError as err: - _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err) + _LOGGER.error( + "Couldn't connect using %s DB_URL: %s", + redact_credentials(db_url), + redact_credentials(str(err)), + ) return finally: - sess.close() + if sess: + sess.close() queries = [] @@ -147,7 +161,11 @@ class SQLSensor(SensorEntity): value = str(value) self._attributes[key] = value except sqlalchemy.exc.SQLAlchemyError as err: - _LOGGER.error("Error executing query %s: %s", self._query, err) + _LOGGER.error( + "Error executing query %s: %s", + self._query, + redact_credentials(str(err)), + ) return finally: sess.close() diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index ddab7b1ba36..11f59444c2c 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -55,3 +55,43 @@ async def test_invalid_query(hass): state = hass.states.get("sensor.count_tables") assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "url,expected_patterns,not_expected_patterns", + [ + ( + "sqlite://homeassistant:hunter2@homeassistant.local", + ["sqlite://****:****@homeassistant.local"], + ["sqlite://homeassistant:hunter2@homeassistant.local"], + ), + ( + "sqlite://homeassistant.local", + ["sqlite://homeassistant.local"], + [], + ), + ], +) +async def test_invalid_url(hass, caplog, url, expected_patterns, not_expected_patterns): + """Test credentials in url is not logged.""" + config = { + "sensor": { + "platform": "sql", + "db_url": url, + "queries": [ + { + "name": "count_tables", + "query": "SELECT 5 as value", + "column": "value", + } + ], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + for pattern in not_expected_patterns: + assert pattern not in caplog.text + for pattern in expected_patterns: + assert pattern in caplog.text From c9f8861303d5a0cb2868305a3a9233156f4d574b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Apr 2021 11:12:31 +0200 Subject: [PATCH 048/120] Fix whitespace error in cast (#48763) --- homeassistant/components/cast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 25b2674821d..b6ca8dd0728 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -186,7 +186,7 @@ class CastDevice(MediaPlayerEntity): ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) self.async_set_cast_info(self._cast_info) - # asyncio.create_task is used to avoid delaying startup wrapup if the device + # asyncio.create_task is used to avoid delaying startup wrapup if the device # is discovered already during startup but then fails to respond asyncio.create_task( async_create_catching_coro(self.async_connect_to_chromecast()) From b382de96c6f025e6935b0ebf6708bdbef66e7682 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Apr 2021 11:42:12 +0200 Subject: [PATCH 049/120] Update frontend to 20210407.0 (#48765) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b659ec7e7d4..79bf552a5e2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210406.0" + "home-assistant-frontend==20210407.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4296705c54f..abd537a12a5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210406.0 +home-assistant-frontend==20210407.0 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index a39bc00d061..9159c3798ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210406.0 +home-assistant-frontend==20210407.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f1ac7757ff..55141db5ca7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210406.0 +home-assistant-frontend==20210407.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0fb4f31bde094824705a875db994b885fc009ec4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Apr 2021 12:43:04 +0200 Subject: [PATCH 050/120] Bumped version to 2021.4.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2d772e9fa3c..ce295d30ca7 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 = "0b5" +PATCH_VERSION = "0b6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From b4636f17fbedad932c78c2b10535db065639fa1f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 Apr 2021 18:00:42 +0200 Subject: [PATCH 051/120] Reject nan, inf from generic_thermostat sensor (#48771) --- .../components/generic_thermostat/climate.py | 6 +++++- tests/components/generic_thermostat/test_climate.py | 13 +++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index ef3cf11fa1c..e83852d122f 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -1,6 +1,7 @@ """Adds support for generic thermostat units.""" import asyncio import logging +import math import voluptuous as vol @@ -419,7 +420,10 @@ class GenericThermostat(ClimateEntity, RestoreEntity): def _async_update_temp(self, state): """Update thermostat with latest state from sensor.""" try: - self._cur_temp = float(state.state) + cur_temp = float(state.state) + if math.isnan(cur_temp) or math.isinf(cur_temp): + raise ValueError(f"Sensor has illegal state {state.state}") + self._cur_temp = cur_temp except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index c2c1435464e..f5a27ac8b97 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -331,9 +331,18 @@ async def test_sensor_bad_value(hass, setup_comp_2): _setup_sensor(hass, None) await hass.async_block_till_done() - state = hass.states.get(ENTITY) - assert temp == state.attributes.get("current_temperature") + assert state.attributes.get("current_temperature") == temp + + _setup_sensor(hass, "inf") + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("current_temperature") == temp + + _setup_sensor(hass, "nan") + await hass.async_block_till_done() + state = hass.states.get(ENTITY) + assert state.attributes.get("current_temperature") == temp async def test_sensor_unknown(hass): From 49178d686509f3492ccf6a4fc5fbf5beec287b14 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Apr 2021 17:57:45 +0200 Subject: [PATCH 052/120] Update frontend to 20210407.1 (#48778) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 79bf552a5e2..b910c0acc46 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210407.0" + "home-assistant-frontend==20210407.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index abd537a12a5..cc8ad8f819e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210407.0 +home-assistant-frontend==20210407.1 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 9159c3798ac..6ac22d380e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210407.0 +home-assistant-frontend==20210407.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55141db5ca7..6edc45e981a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210407.0 +home-assistant-frontend==20210407.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 341531146dc07247ca8e69ca69a7100e90a791d9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Apr 2021 18:31:01 +0200 Subject: [PATCH 053/120] Bumped version to 2021.4.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ce295d30ca7..01efb971fdc 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 = "0b6" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 8d738cff413ace26c5722a294b7020944795ea17 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 8 Apr 2021 10:53:20 -0400 Subject: [PATCH 054/120] Check all endpoints for zwave_js.climate fan mode and operating state (#48800) * Check all endpoints for zwave_js.climate fan mode and operating state * fix test --- homeassistant/components/zwave_js/climate.py | 2 + tests/components/zwave_js/test_climate.py | 4 +- tests/components/zwave_js/test_services.py | 13 +- ..._ct100_plus_different_endpoints_state.json | 1807 ++++++++++------- 4 files changed, 1096 insertions(+), 730 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index b814aef2a9d..c64a5ef788f 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -169,11 +169,13 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): THERMOSTAT_MODE_PROPERTY, CommandClass.THERMOSTAT_FAN_MODE, add_to_watched_value_ids=True, + check_all_endpoints=True, ) self._fan_state = self.get_zwave_value( THERMOSTAT_OPERATING_STATE_PROPERTY, CommandClass.THERMOSTAT_FAN_STATE, add_to_watched_value_ids=True, + check_all_endpoints=True, ) self._set_modes_and_presets() self._supported_features = 0 diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 5631798fc15..83a607f3add 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -348,7 +348,9 @@ async def test_thermostat_different_endpoints( """Test an entity with values on a different endpoint from the primary value.""" state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.8 + assert state.attributes[ATTR_FAN_MODE] == "Auto low" + assert state.attributes[ATTR_FAN_STATE] == "Idle / off" 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 b5a5f8f48f0..7bdba7894d2 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -495,7 +495,7 @@ async def test_poll_value( assert args["valueId"] == { "commandClassName": "Thermostat Mode", "commandClass": 64, - "endpoint": 0, + "endpoint": 1, "property": "mode", "propertyName": "mode", "metadata": { @@ -503,19 +503,16 @@ async def test_poll_value( "readable": True, "writeable": True, "min": 0, - "max": 31, + "max": 255, "label": "Thermostat mode", "states": { "0": "Off", "1": "Heat", "2": "Cool", - "3": "Auto", - "11": "Energy heat", - "12": "Energy cool", }, }, - "value": 1, - "ccVersion": 2, + "value": 2, + "ccVersion": 0, } client.async_send_command.reset_mock() @@ -531,7 +528,7 @@ async def test_poll_value( }, blocking=True, ) - assert len(client.async_send_command.call_args_list) == 8 + assert len(client.async_send_command.call_args_list) == 7 # Test polling against an invalid entity raises ValueError with pytest.raises(ValueError): diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index fcdd57e981b..f940dd210aa 100644 --- a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -1,722 +1,1087 @@ { - "nodeId": 26, - "index": 0, - "installerIcon": 4608, - "userIcon": 4608, - "status": 4, - "ready": true, - "deviceClass": { - "basic": {"key": 4, "label":"Routing Slave"}, - "generic": {"key": 8, "label":"Thermostat"}, - "specific": {"key": 6, "label":"Thermostat General V2"}, - "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] - }, - "isListening": true, - "isFrequentListening": false, - "isRouting": true, - "maxBaudRate": 40000, - "isSecure": false, - "version": 4, - "isBeaming": true, - "manufacturerId": 152, - "productId": 256, - "productType": 25602, - "firmwareVersion": "10.7", - "zwavePlusVersion": 1, - "nodeType": 0, - "roleType": 5, - "deviceConfig": { - "manufacturerId": 152, - "manufacturer": "Radio Thermostat Company of America (RTC)", - "label": "CT100 Plus", - "description": "Z-Wave Thermostat", - "devices": [{ "productType": "0x6402", "productId": "0x0100" }], - "firmwareVersion": { "min": "0.0", "max": "255.255" }, - "paramInformation": { "_map": {} } - }, - "label": "CT100 Plus", - "neighbors": [1, 2, 3, 4, 23], - "endpointCountIsDynamic": false, - "endpointsHaveIdenticalCapabilities": false, - "individualEndpointCount": 2, - "aggregatedEndpointCount": 0, - "interviewAttempts": 1, - "endpoints": [ - { - "nodeId": 26, - "index": 0, - "installerIcon": 4608, - "userIcon": 4608 - }, - { "nodeId": 26, "index": 1 }, - { - "nodeId": 26, - "index": 2, - "installerIcon": 3328, - "userIcon": 3333 - } - ], - "commandClasses": [], - "values": [ - { - "commandClassName": "Manufacturer Specific", - "commandClass": 114, - "endpoint": 0, - "property": "manufacturerId", - "propertyName": "manufacturerId", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Manufacturer ID" - }, - "value": 152, - "ccVersion": 2 - }, - { - "commandClassName": "Manufacturer Specific", - "commandClass": 114, - "endpoint": 0, - "property": "productType", - "propertyName": "productType", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Product type" - }, - "value": 25602, - "ccVersion": 2 - }, - { - "commandClassName": "Manufacturer Specific", - "commandClass": 114, - "endpoint": 0, - "property": "productId", - "propertyName": "productId", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 65535, - "label": "Product ID" - }, - "value": 256, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Mode", - "commandClass": 64, - "endpoint": 0, - "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "min": 0, - "max": 31, - "label": "Thermostat mode", - "states": { - "0": "Off", - "1": "Heat", - "2": "Cool", - "3": "Auto", - "11": "Energy heat", - "12": "Energy cool" - } - }, - "value": 1, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Mode", - "commandClass": 64, - "endpoint": 0, - "property": "manufacturerData", - "propertyName": "manufacturerData", - "metadata": { - "type": "any", - "readable": true, - "writeable": true - }, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 0, - "property": "setpoint", - "propertyKey": 1, - "propertyName": "setpoint", - "propertyKeyName": "Heating", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "unit": "\u00b0F", - "ccSpecific": { "setpointType": 1 } - }, - "value": 72, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 0, - "property": "setpoint", - "propertyKey": 2, - "propertyName": "setpoint", - "propertyKeyName": "Cooling", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "unit": "\u00b0F", - "ccSpecific": { "setpointType": 2 } - }, - "value": 73, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 0, - "property": "setpoint", - "propertyKey": 11, - "propertyName": "setpoint", - "propertyKeyName": "Energy Save Heating", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "unit": "\u00b0F", - "ccSpecific": { "setpointType": 11 } - }, - "value": 62, - "ccVersion": 2 - }, - { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 0, - "property": "setpoint", - "propertyKey": 12, - "propertyName": "setpoint", - "propertyKeyName": "Energy Save Cooling", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "unit": "\u00b0F", - "ccSpecific": { "setpointType": 12 } - }, - "value": 85, - "ccVersion": 2 - }, - { - "commandClassName": "Version", - "commandClass": 134, - "endpoint": 0, - "property": "libraryType", - "propertyName": "libraryType", - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Library type" - }, - "value": 3, - "ccVersion": 2 - }, - { - "commandClassName": "Version", - "commandClass": 134, - "endpoint": 0, - "property": "protocolVersion", - "propertyName": "protocolVersion", - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave protocol version" - }, - "value": "4.24", - "ccVersion": 2 - }, - { - "commandClassName": "Version", - "commandClass": 134, - "endpoint": 0, - "property": "firmwareVersions", - "propertyName": "firmwareVersions", - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave chip firmware versions" - }, - "value": ["10.7"], - "ccVersion": 2 - }, - { - "commandClassName": "Version", - "commandClass": 134, - "endpoint": 0, - "property": "hardwareVersion", - "propertyName": "hardwareVersion", - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Z-Wave chip hardware version" - }, - "ccVersion": 2 - }, - { - "commandClassName": "Indicator", - "commandClass": 135, - "endpoint": 0, - "property": "value", - "propertyName": "value", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "min": 0, - "max": 255, - "label": "Indicator value", - "ccSpecific": { "indicatorId": 0 } - }, - "value": 0, - "ccVersion": 1 - }, - { - "commandClassName": "Thermostat Operating State", - "commandClass": 66, - "endpoint": 0, - "property": "state", - "propertyName": "state", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 255, - "label": "Operating state", - "states": { - "0": "Idle", - "1": "Heating", - "2": "Cooling", - "3": "Fan Only", - "4": "Pending Heat", - "5": "Pending Cool", - "6": "Vent/Economizer", - "7": "Aux Heating", - "8": "2nd Stage Heating", - "9": "2nd Stage Cooling", - "10": "2nd Stage Aux Heat", - "11": "3rd Stage Aux Heat" - } - }, - "value": 0, - "ccVersion": 2 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 1, - "propertyName": "Temperature Reporting Threshold", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 4, - "default": 2, - "format": 0, - "allowManualEntry": false, - "states": { - "0": "Disabled", - "1": "0.5\u00b0 F", - "2": "1.0\u00b0 F", - "3": "1.5\u00b0 F", - "4": "2.0\u00b0 F" - }, - "label": "Temperature Reporting Threshold", - "description": "Reporting threshold for changes in the ambient temperature", - "isFromConfig": true - }, - "value": 2, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 2, - "propertyName": "HVAC Settings", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "valueSize": 4, - "min": 0, - "max": 0, - "default": 0, - "format": 0, - "allowManualEntry": true, - "label": "HVAC Settings", - "description": "Configured HVAC settings", - "isFromConfig": true - }, - "value": 17891329, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 4, - "propertyName": "Power Status", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "valueSize": 1, - "min": 0, - "max": 0, - "default": 0, - "format": 0, - "allowManualEntry": true, - "label": "Power Status", - "description": "C-Wire / Battery Status", - "isFromConfig": true - }, - "value": 1, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 5, - "propertyName": "Humidity Reporting Threshold", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, - "default": 0, - "format": 1, - "allowManualEntry": false, - "states": { - "0": "Disabled", - "1": "3% RH", - "2": "5% RH", - "3": "10% RH" - }, - "label": "Humidity Reporting Threshold", - "description": "Reporting threshold for changes in the relative humidity", - "isFromConfig": true - }, - "value": 2, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 6, - "propertyName": "Auxiliary/Emergency", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, - "default": 0, - "format": 1, - "allowManualEntry": false, - "states": { - "0": "Auxiliary/Emergency heat disabled", - "1": "Auxiliary/Emergency heat enabled" - }, - "label": "Auxiliary/Emergency", - "description": "Enables or disables auxiliary / emergency heating", - "isFromConfig": true - }, - "value": 0, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 7, - "propertyName": "Thermostat Swing Temperature", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 1, - "max": 8, - "default": 2, - "format": 0, - "allowManualEntry": false, - "states": { - "1": "0.5\u00b0 F", - "2": "1.0\u00b0 F", - "3": "1.5\u00b0 F", - "4": "2.0\u00b0 F", - "5": "2.5\u00b0 F", - "6": "3.0\u00b0 F", - "7": "3.5\u00b0 F", - "8": "4.0\u00b0 F" - }, - "label": "Thermostat Swing Temperature", - "description": "Variance allowed from setpoint to engage HVAC", - "isFromConfig": true - }, - "value": 2, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 8, - "propertyName": "Thermostat Diff Temperature", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 4, - "max": 12, - "default": 4, - "format": 0, - "allowManualEntry": false, - "states": { - "4": "2.0\u00b0 F", - "8": "4.0\u00b0 F", - "12": "6.0\u00b0 F" - }, - "label": "Thermostat Diff Temperature", - "description": "Configures additional stages", - "isFromConfig": true - }, - "value": 1028, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 9, - "propertyName": "Thermostat Recovery Mode", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 1, - "max": 2, - "default": 2, - "format": 0, - "allowManualEntry": false, - "states": { - "1": "Fast recovery mode", - "2": "Economy recovery mode" - }, - "label": "Thermostat Recovery Mode", - "description": "Fast or Economy recovery mode", - "isFromConfig": true - }, - "value": 2, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 10, - "propertyName": "Temperature Reporting Filter", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 4, - "min": 0, - "max": 124, - "default": 124, - "format": 0, - "allowManualEntry": true, - "label": "Temperature Reporting Filter", - "description": "Upper/Lower bounds for thermostat temperature reporting", - "isFromConfig": true - }, - "value": 32000, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 11, - "propertyName": "Simple UI Mode", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": false, - "states": { - "0": "Normal mode enabled", - "1": "Simple mode enabled" - }, - "label": "Simple UI Mode", - "description": "Simple mode enable/disable", - "isFromConfig": true - }, - "value": 1, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 12, - "propertyName": "Multicast", - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 1, - "default": 0, - "format": 0, - "allowManualEntry": false, - "states": { - "0": "Multicast disabled", - "1": "Multicast enabled" - }, - "label": "Multicast", - "description": "Enable or disables Multicast", - "isFromConfig": true - }, - "value": 0, - "ccVersion": 1 - }, - { - "commandClassName": "Configuration", - "commandClass": 112, - "endpoint": 0, - "property": 3, - "propertyName": "Utility Lock Enable/Disable", - "metadata": { - "type": "number", - "readable": false, - "writeable": true, - "valueSize": 1, - "min": 0, - "max": 255, - "default": 0, - "format": 1, - "allowManualEntry": false, - "states": { - "0": "Utility lock disabled", - "1": "Utility lock enabled" - }, - "label": "Utility Lock Enable/Disable", - "description": "Prevents setpoint changes at thermostat", - "isFromConfig": true - }, - "ccVersion": 1 - }, - { - "commandClassName": "Battery", - "commandClass": 128, - "endpoint": 0, - "property": "level", - "propertyName": "level", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "min": 0, - "max": 100, - "unit": "%", - "label": "Battery level" - }, - "value": 100, - "ccVersion": 1 - }, - { - "commandClassName": "Battery", - "commandClass": 128, - "endpoint": 0, - "property": "isLow", - "propertyName": "isLow", - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Low battery level" - }, - "value": false, - "ccVersion": 1 - }, - { - "commandClassName": "Multilevel Sensor", - "commandClass": 49, - "endpoint": 2, - "property": "Air temperature", - "propertyName": "Air temperature", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "unit": "\u00b0F", - "label": "Air temperature", - "ccSpecific": { "sensorType": 1, "scale": 1 } - }, - "value": 72.5, - "ccVersion": 5 - }, - { - "commandClassName": "Multilevel Sensor", - "commandClass": 49, - "endpoint": 2, - "property": "Humidity", - "propertyName": "Humidity", - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "unit": "%", - "label": "Humidity", - "ccSpecific": { "sensorType": 5, "scale": 0 } - }, - "value": 20, - "ccVersion": 5 - } - ] -} \ No newline at end of file + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 152, + "productId": 256, + "productType": 25602, + "firmwareVersion": "10.7", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/opt/node_modules/@zwave-js/config/config/devices/0x0098/ct100_plus.json", + "manufacturer": "Radio Thermostat Company of America (RTC)", + "manufacturerId": 152, + "label": "CT100 Plus", + "description": "Z-Wave Thermostat", + "devices": [ + { + "productType": 25602, + "productId": 256 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "CT100 Plus", + "neighbors": [1, 2, 29, 3, 4, 5, 6], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 2, + "aggregatedEndpointCount": 0, + "interviewAttempts": 0, + "interviewStage": 6, + "endpoints": [ + { + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 2, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 0, + "label": "Unused" + }, + "mandatorySupportedCCs": [32, 49], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "\u00b0F" + }, + "value": 73 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Humidity", + "propertyName": "Humidity", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + }, + "unit": "%" + }, + "value": 36 + }, + { + "endpoint": 0, + "commandClass": 66, + "commandClassName": "Thermostat Operating State", + "property": "state", + "propertyName": "state", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Operating state", + "min": 0, + "max": 255, + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 68, + "commandClassName": "Thermostat Fan Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat fan mode", + "min": 0, + "max": 255, + "states": { + "0": "Auto low", + "1": "Low" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 69, + "commandClassName": "Thermostat Fan State", + "property": "state", + "propertyName": "state", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Thermostat fan state", + "min": 0, + "max": 255, + "states": { + "0": "Idle / off", + "1": "Running / running low", + "2": "Running high", + "3": "Running medium", + "4": "Circulation mode", + "5": "Humidity circulation mode", + "6": "Right - left circulation mode", + "7": "Up - down circulation mode", + "8": "Quiet circulation mode" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Temperature Reporting Threshold", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Reporting threshold for changes in the ambient temperature", + "label": "Temperature Reporting Threshold", + "default": 2, + "min": 0, + "max": 4, + "states": { + "0": "Disabled", + "1": "0.5\u00b0 F", + "2": "1.0\u00b0 F", + "3": "1.5\u00b0 F", + "4": "2.0\u00b0 F" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "HVAC Settings", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Configured HVAC settings", + "label": "HVAC Settings", + "default": 0, + "min": 0, + "max": 0, + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 17891329 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Power Status", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "C-Wire / Battery Status", + "label": "Power Status", + "default": 0, + "min": 0, + "max": 0, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Thermostat Swing Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Variance allowed from setpoint to engage HVAC", + "label": "Thermostat Swing Temperature", + "default": 2, + "min": 1, + "max": 8, + "states": { + "1": "0.5\u00b0 F", + "2": "1.0\u00b0 F", + "3": "1.5\u00b0 F", + "4": "2.0\u00b0 F", + "5": "2.5\u00b0 F", + "6": "3.0\u00b0 F", + "7": "3.5\u00b0 F", + "8": "4.0\u00b0 F" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Thermostat Diff Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Configures additional stages", + "label": "Thermostat Diff Temperature", + "default": 4, + "min": 4, + "max": 12, + "states": { + "4": "2.0\u00b0 F", + "8": "4.0\u00b0 F", + "12": "6.0\u00b0 F" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1028 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Thermostat Recovery Mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Fast or Economy recovery mode", + "label": "Thermostat Recovery Mode", + "default": 2, + "min": 1, + "max": 2, + "states": { + "1": "Fast recovery mode", + "2": "Economy recovery mode" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Temperature Reporting Filter", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Upper/Lower bounds for thermostat temperature reporting", + "label": "Temperature Reporting Filter", + "default": 124, + "min": 0, + "max": 124, + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Simple UI Mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Simple mode enable/disable", + "label": "Simple UI Mode", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Normal mode enabled", + "1": "Simple mode enabled" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Multicast", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Enable or disables Multicast", + "label": "Multicast", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Multicast disabled", + "1": "Multicast enabled" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Utility Lock Enable/Disable", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "description": "Prevents setpoint changes at thermostat", + "label": "Utility Lock Enable/Disable", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Utility lock disabled", + "1": "Utility lock enabled" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Humidity Reporting Threshold", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Reporting threshold for changes in the relative humidity", + "label": "Humidity Reporting Threshold", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Disabled", + "1": "3% RH", + "2": "5% RH", + "3": "10% RH" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Auxiliary/Emergency", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Enables or disables auxiliary / emergency heating", + "label": "Auxiliary/Emergency", + "default": 0, + "min": 0, + "max": 255, + "states": { + "0": "Auxiliary/Emergency heat disabled", + "1": "Auxiliary/Emergency heat enabled" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 152 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 25602 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 256 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.24" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["10.7"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255 + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 1, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 152 + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 25602 + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 256 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool" + } + }, + "value": 2 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 1 + }, + "unit": "\u00b0F" + }, + "value": 72 + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 2 + }, + "unit": "\u00b0F" + }, + "value": 73 + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + } + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + } + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + } + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] +} From 99ef87090882f6625997b8ebd13819cb71c54272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Nenz=C3=A9n?= Date: Thu, 8 Apr 2021 17:00:49 +0200 Subject: [PATCH 055/120] Add missing super call in Verisure Camera entity (#48812) --- homeassistant/components/verisure/camera.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index cb159027c16..e667829bb10 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -54,6 +54,7 @@ class VerisureSmartcam(CoordinatorEntity, Camera): ): """Initialize Verisure File Camera component.""" super().__init__(coordinator) + Camera.__init__(self) self.serial_number = serial_number self._directory_path = directory_path From dd2a73b363aabf028869db680902e2f3388257b8 Mon Sep 17 00:00:00 2001 From: Niccolo Zapponi Date: Thu, 8 Apr 2021 12:50:46 +0100 Subject: [PATCH 056/120] Fix iCloud extra attributes (#48815) --- homeassistant/components/icloud/account.py | 2 +- homeassistant/components/icloud/device_tracker.py | 2 +- homeassistant/components/icloud/sensor.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index a357df39e42..5c3bd2bf519 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -501,6 +501,6 @@ class IcloudDevice: return self._location @property - def exta_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, any]: """Return the attributes.""" return self._attrs diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 3dbc10bcf1b..502c2b00f8b 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -110,7 +110,7 @@ class IcloudTrackerEntity(TrackerEntity): @property def extra_state_attributes(self) -> dict[str, any]: """Return the device state attributes.""" - return self._device.state_attributes + return self._device.extra_state_attributes @property def device_info(self) -> dict[str, any]: diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index ddd3d54c556..f889495af25 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -93,7 +93,7 @@ class IcloudDeviceBatterySensor(SensorEntity): @property def extra_state_attributes(self) -> dict[str, any]: """Return default attributes for the iCloud device entity.""" - return self._device.state_attributes + return self._device.extra_state_attributes @property def device_info(self) -> dict[str, any]: From cf11d9a2df759870e2059a0df83f0c575faf7e40 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Apr 2021 21:44:17 +0200 Subject: [PATCH 057/120] Replace redacted stream recorder credentials with '****' (#48832) --- homeassistant/components/stream/__init__.py | 15 ++++++++------- homeassistant/components/stream/worker.py | 6 ++---- tests/components/stream/test_recorder.py | 2 +- tests/components/stream/test_worker.py | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 0226bb82f6d..0d91b63844e 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -39,7 +39,12 @@ from .hls import async_setup_hls _LOGGER = logging.getLogger(__name__) -STREAM_SOURCE_RE = re.compile("//(.*):(.*)@") +STREAM_SOURCE_RE = re.compile("//.*:.*@") + + +def redact_credentials(data): + """Redact credentials from string data.""" + return STREAM_SOURCE_RE.sub("//****:****@", data) def create_stream(hass, stream_source, options=None): @@ -176,9 +181,7 @@ class Stream: target=self._run_worker, ) self._thread.start() - _LOGGER.info( - "Started stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source)) - ) + _LOGGER.info("Started stream: %s", redact_credentials(str(self.source))) def update_source(self, new_source): """Restart the stream with a new stream source.""" @@ -244,9 +247,7 @@ class Stream: self._thread_quit.set() self._thread.join() self._thread = None - _LOGGER.info( - "Stopped stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source)) - ) + _LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source))) async def async_record(self, video_path, duration=30, lookback=5): """Make a .mp4 recording from a provided stream.""" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 5a129356983..cd4528b3088 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -5,7 +5,7 @@ import logging import av -from . import STREAM_SOURCE_RE +from . import redact_credentials from .const import ( AUDIO_CODECS, MAX_MISSING_DTS, @@ -128,9 +128,7 @@ def stream_worker(source, options, segment_buffer, quit_event): try: container = av.open(source, options=options, timeout=STREAM_TIMEOUT) except av.AVError: - _LOGGER.error( - "Error opening stream %s", STREAM_SOURCE_RE.sub("//", str(source)) - ) + _LOGGER.error("Error opening stream %s", redact_credentials(str(source))) return try: video_stream = container.streams.video[0] diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 564da4b108e..5ee055754b9 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -266,4 +266,4 @@ async def test_recorder_log(hass, caplog): with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") assert "https://abcd:efgh@foo.bar" not in caplog.text - assert "https://foo.bar" in caplog.text + assert "https://****:****@foo.bar" in caplog.text diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index cf72a90168b..d5527105a70 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -588,4 +588,4 @@ async def test_worker_log(hass, caplog): ) await hass.async_block_till_done() assert "https://abcd:efgh@foo.bar" not in caplog.text - assert "https://foo.bar" in caplog.text + assert "https://****:****@foo.bar" in caplog.text From f39afa60aeeac0a7ae5a3925b1e80e7ab4729c0f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 Apr 2021 21:39:03 +0200 Subject: [PATCH 058/120] Fix mysensor cover closed state (#48833) --- homeassistant/components/mysensors/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 0e3478a57bf..33393f08def 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -70,12 +70,12 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): else: amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0 + if amount == 0: + return CoverState.CLOSED if v_up and not v_down and not v_stop: return CoverState.OPENING if not v_up and v_down and not v_stop: return CoverState.CLOSING - if not v_up and not v_down and v_stop and amount == 0: - return CoverState.CLOSED return CoverState.OPEN @property From 3f0d63c1ab7b2a34c100c4ee1440d2df7377eb74 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Apr 2021 21:40:48 +0200 Subject: [PATCH 059/120] Validate supported_color_modes for MQTT JSON light (#48836) --- homeassistant/components/light/__init__.py | 14 ++++++++++ .../components/mqtt/light/schema_json.py | 6 ++++- tests/components/mqtt/test_light_json.py | 27 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 4fae5caab00..fe9a38d12b4 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -73,6 +73,20 @@ VALID_COLOR_MODES = { COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF} COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_XY} + +def valid_supported_color_modes(color_modes): + """Validate the given color modes.""" + color_modes = set(color_modes) + if ( + not color_modes + or COLOR_MODE_UNKNOWN in color_modes + or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1) + or (COLOR_MODE_ONOFF in color_modes and len(color_modes) > 1) + ): + raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}") + return color_modes + + # Float that represents transition time in seconds to make change. ATTR_TRANSITION = "transition" diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 8be3708bd61..aaf12f3362f 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -35,6 +35,7 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, VALID_COLOR_MODES, LightEntity, + valid_supported_color_modes, ) from homeassistant.const import ( CONF_BRIGHTNESS, @@ -130,7 +131,10 @@ PLATFORM_SCHEMA_JSON = vol.All( vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All( - cv.ensure_list, [vol.In(VALID_COLOR_MODES)], vol.Unique() + cv.ensure_list, + [vol.In(VALID_COLOR_MODES)], + vol.Unique(), + valid_supported_color_modes, ), vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 7856eb84c07..6c9c7ae903a 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -188,6 +188,33 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): assert hass.states.get("light.test") is None +@pytest.mark.parametrize( + "supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]] +) +async def test_fail_setup_if_color_modes_invalid( + hass, mqtt_mock, supported_color_modes +): + """Test if setup fails if supported color modes is invalid.""" + config = { + light.DOMAIN: { + "brightness": True, + "color_mode": True, + "command_topic": "test_light_rgb/set", + "name": "test", + "platform": "mqtt", + "schema": "json", + "supported_color_modes": supported_color_modes, + } + } + assert await async_setup_component( + hass, + light.DOMAIN, + config, + ) + await hass.async_block_till_done() + assert hass.states.get("light.test") is None + + async def test_rgb_light(hass, mqtt_mock): """Test RGB light flags brightness support.""" assert await async_setup_component( From 0ad473634916bfa29b5106f1386d9f47958a90a3 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 8 Apr 2021 17:35:02 +0200 Subject: [PATCH 060/120] Bump speedtest-cli to 2.1.3 (#48861) --- homeassistant/components/speedtestdotnet/manifest.json | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index d230f03f954..f2e2a2196c9 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -3,6 +3,8 @@ "name": "Speedtest.net", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", - "requirements": ["speedtest-cli==2.1.2"], + "requirements": [ + "speedtest-cli==2.1.3" + ], "codeowners": ["@rohankapoorcom", "@engrbm87"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6ac22d380e2..c5c9adcc892 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2108,7 +2108,7 @@ sonarr==0.3.0 speak2mary==1.4.0 # homeassistant.components.speedtestdotnet -speedtest-cli==2.1.2 +speedtest-cli==2.1.3 # homeassistant.components.spider spiderpy==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6edc45e981a..f2c3e68fce8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1095,7 +1095,7 @@ sonarr==0.3.0 speak2mary==1.4.0 # homeassistant.components.speedtestdotnet -speedtest-cli==2.1.2 +speedtest-cli==2.1.3 # homeassistant.components.spider spiderpy==1.4.2 From 1c939fc9be43ebdc99ee5db7eda7c9558f2c8324 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 8 Apr 2021 17:54:13 +0200 Subject: [PATCH 061/120] Correct wrong x in frontend manifest (#48865) --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 20369503f5c..0529fd6dbb2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -62,7 +62,7 @@ MANIFEST_JSON = { "screenshots": [ { "src": "/static/images/screenshots/screenshot-1.png", - "sizes": "413×792", + "sizes": "413x792", "type": "image/png", } ], From f791142c758e2cea62f1bf6d56d9e27016687cb6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 8 Apr 2021 21:42:56 +0200 Subject: [PATCH 062/120] Fix motion_blinds gateway signal strength sensor (#48866) Co-authored-by: Martin Hjelmare --- homeassistant/components/motion_blinds/sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index d7f40337cec..0da38795f7b 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -183,10 +183,14 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): if self.coordinator.data is None: return False - if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]: - return False + gateway_available = self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE] + if self._device_type == TYPE_GATEWAY: + return gateway_available - return self.coordinator.data[self._device.mac][ATTR_AVAILABLE] + return ( + gateway_available + and self.coordinator.data[self._device.mac][ATTR_AVAILABLE] + ) @property def unit_of_measurement(self): From 02cd2619bb95e9f0e1b6bb340e379f5af7ce15e2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 8 Apr 2021 19:03:11 +0200 Subject: [PATCH 063/120] Fix possibly missing changed_by in Verisure Alarm (#48867) --- homeassistant/components/verisure/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 34a60b9cae4..1cefd6af272 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -112,7 +112,7 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): self._state = ALARM_STATE_TO_HA.get( self.coordinator.data["alarm"]["statusType"] ) - self._changed_by = self.coordinator.data["alarm"]["name"] + self._changed_by = self.coordinator.data["alarm"].get("name") super()._handle_coordinator_update() async def async_added_to_hass(self) -> None: From d9c1c391bca0d74e959fca218c204cfd66533988 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 8 Apr 2021 18:02:29 +0200 Subject: [PATCH 064/120] Fix optional data payload in Prowl messaging service (#48868) --- homeassistant/components/prowl/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index 725c3b9de30..802679ab03d 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -48,7 +48,7 @@ class ProwlNotificationService(BaseNotificationService): "description": message, "priority": data["priority"] if data and "priority" in data else 0, } - if data.get("url"): + if data and data.get("url"): payload["url"] = data["url"] _LOGGER.debug("Attempting call Prowl service at %s", url) From 7051cc04bd858622dc74d61a088d51140fb870cd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 8 Apr 2021 21:41:40 +0200 Subject: [PATCH 065/120] Update frontend to 20210407.2 (#48888) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b910c0acc46..98ae51341af 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210407.1" + "home-assistant-frontend==20210407.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc8ad8f819e..a42e0016924 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210407.1 +home-assistant-frontend==20210407.2 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index c5c9adcc892..954cbb80bd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210407.1 +home-assistant-frontend==20210407.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2c3e68fce8..28d5932284b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210407.1 +home-assistant-frontend==20210407.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 3bf693e352ec0a5284f344b434816f53b687a2cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 8 Apr 2021 21:35:53 +0000 Subject: [PATCH 066/120] Bumped version to 2021.4.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 01efb971fdc..6848123e820 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 = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From 41fe8b9494d23c9660e5f96b4b6cb832787fa64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20Kr=C3=B6ner?= Date: Thu, 8 Apr 2021 13:39:53 +0200 Subject: [PATCH 067/120] Account for openweathermap 'dew_point' not always being present (#48826) --- .../openweathermap/weather_update_coordinator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 51e475eb754..20cc71da725 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -122,7 +122,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_API_FEELS_LIKE_TEMPERATURE: current_weather.temperature("celsius").get( "feels_like" ), - ATTR_API_DEW_POINT: (round(current_weather.dewpoint / 100, 1)), + ATTR_API_DEW_POINT: self._fmt_dewpoint(current_weather.dewpoint), ATTR_API_PRESSURE: current_weather.pressure.get("press"), ATTR_API_HUMIDITY: current_weather.humidity, ATTR_API_WIND_BEARING: current_weather.wind().get("deg"), @@ -178,6 +178,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return forecast + @staticmethod + def _fmt_dewpoint(dewpoint): + if dewpoint is not None: + return round(dewpoint / 100, 1) + return None + @staticmethod def _get_rain(rain): """Get rain data from weather data.""" From 6746fbadef9cc0bd99df66a2b2504afb2149fdb5 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 9 Apr 2021 01:43:41 +0200 Subject: [PATCH 068/120] Catch expected errors and log them in rituals perfume genie (#48870) * Add update error logging * Move try available to else * Remove TimeoutError --- .../components/rituals_perfume_genie/switch.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index 471be52b054..bc8e2b5e175 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,10 +1,15 @@ """Support for Rituals Perfume Genie switches.""" from datetime import timedelta +import logging + +import aiohttp from homeassistant.components.switch import SwitchEntity from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + SCAN_INTERVAL = timedelta(seconds=30) ON_STATE = "1" @@ -33,6 +38,7 @@ class DiffuserSwitch(SwitchEntity): def __init__(self, diffuser): """Initialize the switch.""" self._diffuser = diffuser + self._available = True @property def device_info(self): @@ -53,7 +59,7 @@ class DiffuserSwitch(SwitchEntity): @property def available(self): """Return if the device is available.""" - return self._diffuser.data["hub"]["status"] == AVAILABLE_STATE + return self._available @property def name(self): @@ -89,4 +95,10 @@ class DiffuserSwitch(SwitchEntity): async def async_update(self): """Update the data of the device.""" - await self._diffuser.update_data() + try: + await self._diffuser.update_data() + except aiohttp.ClientError: + self._available = False + _LOGGER.error("Unable to retrieve data from rituals.sense-company.com") + else: + self._available = self._diffuser.data["hub"]["status"] == AVAILABLE_STATE From 1f21b19eaeab71867518a3c44527707a893a5e93 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Apr 2021 17:19:23 +0200 Subject: [PATCH 069/120] Extend media source URL expiry to 24h (#48912) --- homeassistant/components/media_source/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 6aa01403a5f..0ef5d460580 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -19,6 +19,8 @@ from . import local_source, models from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX from .error import Unresolvable +DEFAULT_EXPIRY_TIME = 3600 * 24 + def is_media_source_id(media_content_id: str): """Test if identifier is a media source.""" @@ -105,7 +107,7 @@ async def websocket_browse_media(hass, connection, msg): { vol.Required("type"): "media_source/resolve_media", vol.Required(ATTR_MEDIA_CONTENT_ID): str, - vol.Optional("expires", default=30): int, + vol.Optional("expires", default=DEFAULT_EXPIRY_TIME): int, } ) @websocket_api.async_response From 1ca087b4d0cbc7ec20eefa966ea0f7529959e318 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 9 Apr 2021 11:27:43 +0200 Subject: [PATCH 070/120] Bump pykodi to 0.2.4 (#48913) --- homeassistant/components/kodi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 63282ed1a9a..58d46aea8ba 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -3,7 +3,7 @@ "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": [ - "pykodi==0.2.3" + "pykodi==0.2.4" ], "codeowners": [ "@OnFreund", diff --git a/requirements_all.txt b/requirements_all.txt index 954cbb80bd4..89a0c18c542 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1476,7 +1476,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.3 +pykodi==0.2.4 # homeassistant.components.kulersky pykulersky==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28d5932284b..aee9fa9e67f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -784,7 +784,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.3 +pykodi==0.2.4 # homeassistant.components.kulersky pykulersky==0.5.2 From 38a1c65ab79d78bac8fe4fdd346001ec1dfe1937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 9 Apr 2021 18:54:24 +0200 Subject: [PATCH 071/120] Handle exceptions when looking for new version (#48922) --- homeassistant/components/version/sensor.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 9d558f4ba7c..d438f391334 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,7 +1,9 @@ """Sensor that can display the current Home Assistant versions.""" from datetime import timedelta +import logging from pyhaversion import HaVersion, HaVersionChannel, HaVersionSource +from pyhaversion.exceptions import HaVersionFetchException, HaVersionParseException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -59,6 +61,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER: logging.Logger = logging.getLogger(__name__) + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Version sensor platform.""" @@ -114,7 +118,14 @@ class VersionData: @Throttle(TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest version information.""" - await self.api.get_version() + try: + await self.api.get_version() + except HaVersionFetchException as exception: + _LOGGER.warning(exception) + except HaVersionParseException as exception: + _LOGGER.warning( + "Could not parse data received for %s - %s", self.api.source, exception + ) class VersionSensor(SensorEntity): From 29bb6d76f1da29c369280a86069ea53783c7e64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 9 Apr 2021 16:44:02 +0200 Subject: [PATCH 072/120] Change discovery timeout from 10 to 60 (#48924) --- homeassistant/components/hassio/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 90077261185..301d353faf0 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -148,7 +148,7 @@ class HassIO: This method return a coroutine. """ - return self.send_command("/discovery", method="get") + return self.send_command("/discovery", method="get", timeout=60) @api_data def get_discovery_message(self, uuid): From ee78c9b08a88de83d015d323bfbe93cb7b8be4f4 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 9 Apr 2021 19:36:13 +0200 Subject: [PATCH 073/120] Fix "notify.events" trim() issue + add initial tests (#48928) Co-authored-by: Paulus Schoutsen --- .../components/notify_events/notify.py | 5 +-- requirements_test_all.txt | 3 ++ tests/components/notify_events/__init__.py | 1 + tests/components/notify_events/test_init.py | 12 ++++++ tests/components/notify_events/test_notify.py | 38 +++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 tests/components/notify_events/__init__.py create mode 100644 tests/components/notify_events/test_init.py create mode 100644 tests/components/notify_events/test_notify.py diff --git a/homeassistant/components/notify_events/notify.py b/homeassistant/components/notify_events/notify.py index ce7c353badb..51705453edf 100644 --- a/homeassistant/components/notify_events/notify.py +++ b/homeassistant/components/notify_events/notify.py @@ -116,12 +116,9 @@ class NotifyEventsNotificationService(BaseNotificationService): def send_message(self, message, **kwargs): """Send a message.""" - token = self.token data = kwargs.get(ATTR_DATA) or {} + token = data.get(ATTR_TOKEN, self.token) msg = self.prepare_message(message, data) - if data.get(ATTR_TOKEN, "").trim(): - token = data[ATTR_TOKEN] - msg.send(token) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aee9fa9e67f..b0403065e96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -518,6 +518,9 @@ netdisco==2.8.2 # homeassistant.components.nexia nexia==0.9.5 +# homeassistant.components.notify_events +notify-events==1.0.4 + # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 diff --git a/tests/components/notify_events/__init__.py b/tests/components/notify_events/__init__.py new file mode 100644 index 00000000000..5e2f9c2eaf1 --- /dev/null +++ b/tests/components/notify_events/__init__.py @@ -0,0 +1 @@ +"""Tests for the notify_events integration.""" diff --git a/tests/components/notify_events/test_init.py b/tests/components/notify_events/test_init.py new file mode 100644 index 00000000000..861be83a9cc --- /dev/null +++ b/tests/components/notify_events/test_init.py @@ -0,0 +1,12 @@ +"""The tests for notify_events.""" +from homeassistant.components.notify_events.const import DOMAIN +from homeassistant.setup import async_setup_component + + +async def test_setup(hass): + """Test setup of the integration.""" + config = {"notify_events": {"token": "ABC"}} + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + assert DOMAIN in hass.data diff --git a/tests/components/notify_events/test_notify.py b/tests/components/notify_events/test_notify.py new file mode 100644 index 00000000000..55cf6275044 --- /dev/null +++ b/tests/components/notify_events/test_notify.py @@ -0,0 +1,38 @@ +"""The tests for notify_events.""" +from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, DOMAIN +from homeassistant.components.notify_events.notify import ( + ATTR_LEVEL, + ATTR_PRIORITY, + ATTR_TOKEN, +) + +from tests.common import async_mock_service + + +async def test_send_msg(hass): + """Test notify.events service.""" + notify_calls = async_mock_service(hass, DOMAIN, "events") + + await hass.services.async_call( + DOMAIN, + "events", + { + ATTR_MESSAGE: "message content", + ATTR_DATA: { + ATTR_TOKEN: "XYZ", + ATTR_LEVEL: "warning", + ATTR_PRIORITY: "high", + }, + }, + blocking=True, + ) + + assert len(notify_calls) == 1 + call = notify_calls[-1] + + assert call.domain == DOMAIN + assert call.service == "events" + assert call.data.get(ATTR_MESSAGE) == "message content" + assert call.data.get(ATTR_DATA).get(ATTR_TOKEN) == "XYZ" + assert call.data.get(ATTR_DATA).get(ATTR_LEVEL) == "warning" + assert call.data.get(ATTR_DATA).get(ATTR_PRIORITY) == "high" From b41e611cb55721fbe822e867ca28175596e9e8ed Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 9 Apr 2021 18:54:39 +0200 Subject: [PATCH 074/120] Bump pykodi to 0.2.5 (#48930) --- homeassistant/components/kodi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 58d46aea8ba..9ab51050704 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -3,7 +3,7 @@ "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": [ - "pykodi==0.2.4" + "pykodi==0.2.5" ], "codeowners": [ "@OnFreund", diff --git a/requirements_all.txt b/requirements_all.txt index 89a0c18c542..d5225773604 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1476,7 +1476,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.4 +pykodi==0.2.5 # homeassistant.components.kulersky pykulersky==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0403065e96..47ab084b85f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -787,7 +787,7 @@ pykira==0.1.1 pykmtronic==0.0.3 # homeassistant.components.kodi -pykodi==0.2.4 +pykodi==0.2.5 # homeassistant.components.kulersky pykulersky==0.5.2 From b800bb0202b05283f437c5c966f3b3d16fb16255 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 9 Apr 2021 12:08:56 -0400 Subject: [PATCH 075/120] Bump ZHA quirks library (#48931) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 5825bdcda0f..5cd57e26274 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.23.1", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.55", + "zha-quirks==0.0.56", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", "zigpy==0.33.0", diff --git a/requirements_all.txt b/requirements_all.txt index d5225773604..e85979a0568 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2372,7 +2372,7 @@ zengge==0.2 zeroconf==0.29.0 # homeassistant.components.zha -zha-quirks==0.0.55 +zha-quirks==0.0.56 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47ab084b85f..fbdb0b9defa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1233,7 +1233,7 @@ zeep[async]==4.0.0 zeroconf==0.29.0 # homeassistant.components.zha -zha-quirks==0.0.55 +zha-quirks==0.0.56 # homeassistant.components.zha zigpy-cc==0.5.2 From 4da77b976857cd864ed5c705f01bd679b0f2b936 Mon Sep 17 00:00:00 2001 From: Ph-Wagner <55734069+Ph-Wagner@users.noreply.github.com> Date: Fri, 9 Apr 2021 18:02:06 +0200 Subject: [PATCH 076/120] Extend Google Cast media source URL expiry to 24h (#48937) * Extend media source URL expiry to 12h closes #46280 After checking out https://github.com/home-assistant/core/pull/48912 I just think why not. * Update homeassistant/components/cast/media_player.py Co-authored-by: Erik Montnemery --- homeassistant/components/cast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b6ca8dd0728..016d5162d23 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -473,7 +473,7 @@ class CastDevice(MediaPlayerEntity): self.hass, refresh_token.id, media_id, - timedelta(minutes=5), + timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), ) # prepend external URL From 92746aa60ce5a6897b8827872d79f1e766116422 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 9 Apr 2021 12:38:01 -0500 Subject: [PATCH 077/120] Fix Plex live TV handling (#48953) --- homeassistant/components/plex/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 731d5bbc7db..af1343095f0 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -7,7 +7,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.util import dt as dt_util -LIVE_TV_SECTION = "-4" +LIVE_TV_SECTION = -4 class PlexSession: From 3f744bcbefeee6f793e9bf5b221f5b20a1838bca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Apr 2021 17:39:03 +0000 Subject: [PATCH 078/120] Bumped version to 2021.4.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6848123e820..ffc2d1a3427 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 = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From d5d9a5ff116ca45fdcfe9cfc3113a332e8a1759f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 9 Apr 2021 20:53:20 +0200 Subject: [PATCH 079/120] Update frontend to 20210407.3 (#48957) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 98ae51341af..b69ee769d66 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210407.2" + "home-assistant-frontend==20210407.3" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a42e0016924..c5f0ccde05d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.42.0 -home-assistant-frontend==20210407.2 +home-assistant-frontend==20210407.3 httpx==0.17.1 jinja2>=2.11.3 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index e85979a0568..694f7c73b89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,7 +763,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210407.2 +home-assistant-frontend==20210407.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fbdb0b9defa..3936ed66128 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -412,7 +412,7 @@ hole==0.5.1 holidays==0.10.5.2 # homeassistant.components.frontend -home-assistant-frontend==20210407.2 +home-assistant-frontend==20210407.3 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 2c7fd3002950af4d0189cefe044e2a125bfc395d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 10 Apr 2021 01:13:07 +0200 Subject: [PATCH 080/120] Add TTS engines in config.components (#48939) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tts/__init__.py | 5 +++++ homeassistant/components/tts/manifest.json | 1 + homeassistant/setup.py | 1 + tests/components/tts/test_init.py | 1 + 4 files changed, 8 insertions(+) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index e0f59c51e5a..5922392f17d 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -31,6 +31,7 @@ from homeassistant.const import ( CONF_PLATFORM, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, + PLATFORM_FORMAT, ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -316,6 +317,10 @@ class SpeechManager: provider.name = engine self.providers[engine] = provider + self.hass.config.components.add( + PLATFORM_FORMAT.format(domain=engine, platform=DOMAIN) + ) + async def async_get_url_path( self, engine, message, cache=None, language=None, options=None ): diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index 3db130d01bc..07cee3b867b 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -5,5 +5,6 @@ "requirements": ["mutagen==1.45.1"], "dependencies": ["http"], "after_dependencies": ["media_player"], + "quality_scale": "internal", "codeowners": ["@pvizeli"] } diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 6b306995dfc..1b48efb8c0f 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -36,6 +36,7 @@ BASE_PLATFORMS = { "scene", "sensor", "switch", + "tts", "vacuum", "water_heater", } diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 77fbd3f7170..8cd1641caa0 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -102,6 +102,7 @@ async def test_setup_component_demo(hass): assert hass.services.has_service(tts.DOMAIN, "demo_say") assert hass.services.has_service(tts.DOMAIN, "clear_cache") + assert f"{tts.DOMAIN}.demo" in hass.config.components async def test_setup_component_demo_no_access_cache_folder(hass, mock_init_cache_dir): From 6296d78e58f2dff95ae068b9600c7987c79dbe62 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 10 Apr 2021 01:14:48 +0200 Subject: [PATCH 081/120] Implement percentage_step and preset_mode is not not speed fix for MQTT fan (#48951) --- homeassistant/components/mqtt/fan.py | 88 ++---- tests/components/mqtt/test_fan.py | 418 ++++++++++----------------- 2 files changed, 172 insertions(+), 334 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 395480a041d..6009b941c5c 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -32,6 +32,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.percentage import ( + int_states_in_range, ordered_list_item_to_percentage, percentage_to_ordered_list_item, percentage_to_ranged_value, @@ -224,6 +225,9 @@ class MqttFan(MqttEntity, FanEntity): self._optimistic_preset_mode = None self._optimistic_speed = None + self._legacy_speeds_list = [] + self._legacy_speeds_list_no_off = [] + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -284,28 +288,18 @@ class MqttFan(MqttEntity, FanEntity): self._legacy_speeds_list_no_off = speed_list_without_preset_modes( self._legacy_speeds_list ) - else: - self._legacy_speeds_list = [] self._feature_percentage = CONF_PERCENTAGE_COMMAND_TOPIC in config self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: - self._speeds_list = speed_list_without_preset_modes( - self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST] - ) - self._preset_modes = ( - self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST] - ) + self._preset_modes = config[CONF_PRESET_MODES_LIST] else: - self._speeds_list = speed_list_without_preset_modes( - self._legacy_speeds_list - ) self._preset_modes = [] - if not self._speeds_list or self._feature_percentage: - self._speed_count = 100 + if self._feature_percentage: + self._speed_count = min(int_states_in_range(self._speed_range), 100) else: - self._speed_count = len(self._speeds_list) + self._speed_count = len(self._legacy_speeds_list_no_off) or 100 optimistic = config[CONF_OPTIMISTIC] self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None @@ -327,11 +321,7 @@ class MqttFan(MqttEntity, FanEntity): self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None and SUPPORT_OSCILLATE ) - if self._feature_preset_mode and self._speeds_list: - self._supported_features |= SUPPORT_SET_SPEED - if self._feature_percentage: - self._supported_features |= SUPPORT_SET_SPEED - if self._feature_legacy_speeds: + if self._feature_percentage or self._feature_legacy_speeds: self._supported_features |= SUPPORT_SET_SPEED if self._feature_preset_mode: self._supported_features |= SUPPORT_PRESET_MODE @@ -414,10 +404,6 @@ class MqttFan(MqttEntity, FanEntity): return self._preset_mode = preset_mode - if not self._implemented_percentage and (preset_mode in self.speed_list): - self._percentage = ordered_list_item_to_percentage( - self.speed_list, preset_mode - ) self.async_write_ha_state() if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None: @@ -455,10 +441,10 @@ class MqttFan(MqttEntity, FanEntity): ) return - if not self._implemented_percentage: - if speed in self._speeds_list: + if not self._feature_percentage: + if speed in self._legacy_speeds_list_no_off: self._percentage = ordered_list_item_to_percentage( - self._speeds_list, speed + self._legacy_speeds_list_no_off, speed ) elif speed == SPEED_OFF: self._percentage = 0 @@ -506,19 +492,9 @@ class MqttFan(MqttEntity, FanEntity): """Return true if device is on.""" return self._state - @property - def _implemented_percentage(self): - """Return true if percentage has been implemented.""" - return self._feature_percentage - - @property - def _implemented_preset_mode(self): - """Return true if preset_mode has been implemented.""" - return self._feature_preset_mode - # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) @property - def _implemented_speed(self): + def _implemented_speed(self) -> bool: """Return true if speed has been implemented.""" return self._feature_legacy_speeds @@ -541,7 +517,7 @@ class MqttFan(MqttEntity, FanEntity): @property def speed_list(self) -> list: """Get the list of available speeds.""" - return self._speeds_list + return self._legacy_speeds_list_no_off @property def supported_features(self) -> int: @@ -555,7 +531,7 @@ class MqttFan(MqttEntity, FanEntity): @property def speed_count(self) -> int: - """Return the number of speeds the fan supports or 100 if percentage is supported.""" + """Return the number of speeds the fan supports.""" return self._speed_count @property @@ -620,20 +596,8 @@ class MqttFan(MqttEntity, FanEntity): percentage_to_ranged_value(self._speed_range, percentage) ) mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload) - if self._implemented_preset_mode: - if percentage: - await self.async_set_preset_mode( - preset_mode=percentage_to_ordered_list_item( - self.speed_list, percentage - ) - ) - # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7) - elif self._feature_legacy_speeds and ( - SPEED_OFF in self._legacy_speeds_list - ): - await self.async_set_preset_mode(SPEED_OFF) # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7) - elif self._feature_legacy_speeds: + if self._feature_legacy_speeds: if percentage: await self.async_set_speed( percentage_to_ordered_list_item( @@ -644,7 +608,7 @@ class MqttFan(MqttEntity, FanEntity): elif SPEED_OFF in self._legacy_speeds_list: await self.async_set_speed(SPEED_OFF) - if self._implemented_percentage: + if self._feature_percentage: mqtt.async_publish( self.hass, self._topic[CONF_PERCENTAGE_COMMAND_TOPIC], @@ -665,13 +629,7 @@ class MqttFan(MqttEntity, FanEntity): if preset_mode not in self.preset_modes: _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) return - # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7) - if preset_mode in self._legacy_speeds_list: - await self.async_set_speed(speed=preset_mode) - if not self._implemented_percentage and preset_mode in self.speed_list: - self._percentage = ordered_list_item_to_percentage( - self.speed_list, preset_mode - ) + mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode) mqtt.async_publish( @@ -693,18 +651,18 @@ class MqttFan(MqttEntity, FanEntity): This method is a coroutine. """ speed_payload = None - if self._feature_legacy_speeds: + if speed in self._legacy_speeds_list: if speed == SPEED_LOW: speed_payload = self._payload["SPEED_LOW"] elif speed == SPEED_MEDIUM: speed_payload = self._payload["SPEED_MEDIUM"] elif speed == SPEED_HIGH: speed_payload = self._payload["SPEED_HIGH"] - elif speed == SPEED_OFF: - speed_payload = self._payload["SPEED_OFF"] else: - _LOGGER.warning("'%s'is not a valid speed", speed) - return + speed_payload = self._payload["SPEED_OFF"] + else: + _LOGGER.warning("'%s' is not a valid speed", speed) + return if speed_payload: mqtt.async_publish( diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index be32540b04d..5caec9b7473 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -85,11 +85,11 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): "preset_mode_state_topic": "preset-mode-state-topic", "preset_mode_command_topic": "preset-mode-command-topic", "preset_modes": [ - "medium", - "medium-high", - "high", - "very-high", - "freaking-high", + "auto", + "smart", + "whoosh", + "eco", + "breeze", "silent", ], "speed_range_min": 1, @@ -126,6 +126,8 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): state = hass.states.get("fan.test") assert state.attributes.get("oscillating") is False + assert state.attributes.get("percentage_step") == 1.0 + async_fire_mqtt_message(hass, "percentage-state-topic", "0") state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 @@ -151,16 +153,16 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): caplog.clear() async_fire_mqtt_message(hass, "preset-mode-state-topic", "low") - state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" + assert "not a valid preset mode" in caplog.text + caplog.clear() - async_fire_mqtt_message(hass, "preset-mode-state-topic", "medium") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "auto") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "medium" + assert state.attributes.get("preset_mode") == "auto" - async_fire_mqtt_message(hass, "preset-mode-state-topic", "very-high") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "eco") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "very-high" + assert state.attributes.get("preset_mode") == "eco" async_fire_mqtt_message(hass, "preset-mode-state-topic", "silent") state = hass.states.get("fan.test") @@ -256,7 +258,9 @@ async def test_controlling_state_via_topic_with_different_speed_range( caplog.clear() -async def test_controlling_state_via_topic_no_percentage_topics(hass, mqtt_mock): +async def test_controlling_state_via_topic_no_percentage_topics( + hass, mqtt_mock, caplog +): """Test the controlling state via topic without percentage topics.""" assert await async_setup_component( hass, @@ -273,9 +277,11 @@ async def test_controlling_state_via_topic_no_percentage_topics(hass, mqtt_mock) "preset_mode_state_topic": "preset-mode-state-topic", "preset_mode_command_topic": "preset-mode-command-topic", "preset_modes": [ - "high", - "freaking-high", - "silent", + "auto", + "smart", + "whoosh", + "eco", + "breeze", ], # use of speeds is deprecated, support will be removed after a quarter (2021.7) "speeds": ["off", "low", "medium"], @@ -288,57 +294,51 @@ async def test_controlling_state_via_topic_no_percentage_topics(hass, mqtt_mock) assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) - async_fire_mqtt_message(hass, "preset-mode-state-topic", "freaking-high") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "smart") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "freaking-high" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 + assert state.attributes.get("preset_mode") == "smart" + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None # use of speeds is deprecated, support will be removed after a quarter (2021.7) assert state.attributes.get("speed") == fan.SPEED_OFF - async_fire_mqtt_message(hass, "preset-mode-state-topic", "high") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "auto") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "high" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 75 + assert state.attributes.get("preset_mode") == "auto" + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None # use of speeds is deprecated, support will be removed after a quarter (2021.7) assert state.attributes.get("speed") == fan.SPEED_OFF - async_fire_mqtt_message(hass, "preset-mode-state-topic", "silent") + async_fire_mqtt_message(hass, "preset-mode-state-topic", "whoosh") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "silent" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 75 + assert state.attributes.get("preset_mode") == "whoosh" + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None # use of speeds is deprecated, support will be removed after a quarter (2021.7) assert state.attributes.get("speed") == fan.SPEED_OFF async_fire_mqtt_message(hass, "preset-mode-state-topic", "medium") - state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "medium" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - assert state.attributes.get("speed") == fan.SPEED_OFF + assert "not a valid preset mode" in caplog.text + caplog.clear() async_fire_mqtt_message(hass, "preset-mode-state-topic", "low") - state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 25 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - assert state.attributes.get("speed") == fan.SPEED_OFF + assert "not a valid preset mode" in caplog.text + caplog.clear() # use of speeds is deprecated, support will be removed after a quarter (2021.7) async_fire_mqtt_message(hass, "speed-state-topic", "medium") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50 + assert state.attributes.get("preset_mode") == "whoosh" + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 assert state.attributes.get("speed") == fan.SPEED_MEDIUM async_fire_mqtt_message(hass, "speed-state-topic", "low") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" - assert state.attributes.get(fan.ATTR_PERCENTAGE) == 25 + assert state.attributes.get("preset_mode") == "whoosh" + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50 assert state.attributes.get("speed") == fan.SPEED_LOW async_fire_mqtt_message(hass, "speed-state-topic", "off") state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "low" + assert state.attributes.get("preset_mode") == "whoosh" assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get("speed") == fan.SPEED_OFF @@ -361,11 +361,11 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap "preset_mode_state_topic": "preset-mode-state-topic", "preset_mode_command_topic": "preset-mode-command-topic", "preset_modes": [ - "medium", - "medium-high", - "high", - "very-high", - "freaking-high", + "auto", + "smart", + "whoosh", + "eco", + "breeze", "silent", ], "state_value_template": "{{ value_json.val }}", @@ -412,20 +412,20 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap assert "not a valid preset mode" in caplog.text caplog.clear() - async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "medium"}') + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "auto"}') state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "medium" + assert state.attributes.get("preset_mode") == "auto" - async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "freaking-high"}') + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "breeze"}') state = hass.states.get("fan.test") - assert state.attributes.get("preset_mode") == "freaking-high" + assert state.attributes.get("preset_mode") == "breeze" async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "silent"}') state = hass.states.get("fan.test") assert state.attributes.get("preset_mode") == "silent" -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -447,8 +447,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): # use of speeds is deprecated, support will be removed after a quarter (2021.7) "speeds": ["off", "low", "medium"], "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", ], # use of speeds is deprecated, support will be removed after a quarter (2021.7) @@ -510,7 +510,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False) mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "freaking-high", 0, False + "speed-command-topic", "speed_mEdium", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -518,11 +518,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 0) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "0", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "off", 0, False - ) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call( "speed-command-topic", "speed_OfF", 0, False @@ -534,54 +531,32 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(fan.ATTR_SPEED) == fan.SPEED_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) + # use of speeds is deprecated, support will be removed after a quarter (2021.7) await common.async_set_preset_mode(hass, "fan.test", "low") - assert mqtt_mock.async_publish.call_count == 2 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call( - "speed-command-topic", "speed_lOw", 0, False - ) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "low", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "low" - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - assert state.attributes.get(fan.ATTR_SPEED) == fan.SPEED_LOW - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid preset mode" in caplog.text + caplog.clear() + # use of speeds is deprecated, support will be removed after a quarter (2021.7) await common.async_set_preset_mode(hass, "fan.test", "medium") - assert mqtt_mock.async_publish.call_count == 2 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call( - "speed-command-topic", "speed_mEdium", 0, False - ) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False + assert "not a valid preset mode" in caplog.text + caplog.clear() + + await common.async_set_preset_mode(hass, "fan.test", "whoosh") + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "medium" - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - assert state.attributes.get(fan.ATTR_SPEED) == fan.SPEED_MEDIUM + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "whoosh" assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "breeze") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "breeze", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "high" - assert state.attributes.get(ATTR_ASSUMED_STATE) - - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") - mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "freaking-high", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "breeze" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_preset_mode(hass, "fan.test", "silent") @@ -615,13 +590,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): # use of speeds is deprecated, support will be removed after a quarter (2021.7) await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH) - mqtt_mock.async_publish.assert_called_once_with( - "speed-command-topic", "speed_High", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid speed" in caplog.text + caplog.clear() # use of speeds is deprecated, support will be removed after a quarter (2021.7) await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF) @@ -735,8 +705,8 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c "percentage_command_topic": "percentage-command-topic", "preset_mode_command_topic": "preset-mode-command-topic", "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", ], } @@ -769,14 +739,12 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c await common.async_set_percentage(hass, "fan.test", 101) await common.async_set_percentage(hass, "fan.test", 100) - mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "freaking-high", 0, False + mqtt_mock.async_publish.assert_called_once_with( + "percentage-command-topic", "100", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 0) @@ -793,26 +761,26 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert "not a valid preset mode" in caplog.text caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "medium") + await common.async_set_preset_mode(hass, "fan.test", "auto") assert "not a valid preset mode" in caplog.text caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "whoosh" assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") + await common.async_set_preset_mode(hass, "fan.test", "breeze") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "freaking-high", 0, False + "preset-mode-command-topic", "breeze", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "breeze" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_preset_mode(hass, "fan.test", "silent") @@ -825,12 +793,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", percentage=25) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "25", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "high", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -843,11 +808,11 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_turn_on(hass, "fan.test", preset_mode="high") + await common.async_turn_on(hass, "fan.test", preset_mode="whoosh") assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -855,7 +820,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert state.attributes.get(ATTR_ASSUMED_STATE) with pytest.raises(NotValidPresetModeError): - await common.async_turn_on(hass, "fan.test", preset_mode="low") + await common.async_turn_on(hass, "fan.test", preset_mode="freaking-high") async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): @@ -876,8 +841,8 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): "preset_mode_command_topic": "preset-mode-command-topic", "preset_mode_command_template": "preset_mode: {{ value }}", "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", ], } @@ -914,16 +879,12 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): await common.async_set_percentage(hass, "fan.test", 101) await common.async_set_percentage(hass, "fan.test", 100) - mqtt_mock.async_publish.assert_any_call( + mqtt_mock.async_publish.assert_called_once_with( "percentage-command-topic", "percentage: 100", 0, False ) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "preset_mode: freaking-high", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 0) @@ -944,22 +905,22 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert "not a valid preset mode" in caplog.text caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "preset_mode: high", 0, False + "preset-mode-command-topic", "preset_mode: whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "whoosh" assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") + await common.async_set_preset_mode(hass, "fan.test", "breeze") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "preset_mode: freaking-high", 0, False + "preset-mode-command-topic", "preset_mode: breeze", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) == "freaking-high" + assert state.attributes.get(fan.ATTR_PRESET_MODE) == "breeze" assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_preset_mode(hass, "fan.test", "silent") @@ -972,14 +933,11 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", percentage=25) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "state: ON", 0, False) mqtt_mock.async_publish.assert_any_call( "percentage-command-topic", "percentage: 25", 0, False ) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "preset_mode: high", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -992,11 +950,11 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_turn_on(hass, "fan.test", preset_mode="high") + await common.async_turn_on(hass, "fan.test", preset_mode="whoosh") assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "state: ON", 0, False) mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "preset_mode: high", 0, False + "preset-mode-command-topic", "preset_mode: whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -1008,7 +966,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( - hass, mqtt_mock + hass, mqtt_mock, caplog ): """Test optimistic mode without state topic without percentage command topic.""" assert await async_setup_component( @@ -1027,9 +985,10 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( # use of speeds is deprecated, support will be removed after a quarter (2021.7) "speeds": ["off", "low", "medium"], "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", + "high", ], } }, @@ -1047,9 +1006,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( await common.async_set_percentage(hass, "fan.test", 101) await common.async_set_percentage(hass, "fan.test", 100) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "freaking-high", 0, False - ) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 @@ -1063,41 +1020,27 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert mqtt_mock.async_publish.call_count == 2 # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "low", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) is None - assert state.attributes.get(ATTR_ASSUMED_STATE) + await common.async_set_preset_mode(hass, "fan.test", "low") + assert "not a valid preset mode" in caplog.text + caplog.clear() await common.async_set_preset_mode(hass, "fan.test", "medium") - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False + assert "not a valid preset mode" in caplog.text + caplog.clear() + + await common.async_set_preset_mode(hass, "fan.test", "whoosh") + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PRESET_MODE) is None assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "breeze") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "high", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.attributes.get(fan.ATTR_PRESET_MODE) is None - assert state.attributes.get(ATTR_ASSUMED_STATE) - - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") - mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "freaking-high", 0, False + "preset-mode-command-topic", "breeze", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -1133,14 +1076,8 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH) - mqtt_mock.async_publish.assert_called_once_with( - "speed-command-topic", "high", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) - + assert "not a valid speed" in caplog.text + caplog.clear() await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "off", 0, False) @@ -1150,13 +1087,10 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", speed="medium") - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -1325,8 +1259,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca # use of speeds is deprecated, support will be removed after a quarter (2021.7) "speeds": ["off", "low", "medium"], "preset_modes": [ - "high", - "freaking-high", + "whoosh", + "breeze", "silent", ], "optimistic": True, @@ -1358,9 +1292,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) + mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -1374,11 +1306,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", percentage=25) - assert mqtt_mock.async_publish.call_count == 4 + assert mqtt_mock.async_publish.call_count == 3 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "low", 0, False - ) mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "25", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) @@ -1394,24 +1323,15 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_turn_on(hass, "fan.test", preset_mode="medium") - assert mqtt_mock.async_publish.call_count == 3 - mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_ON - assert state.attributes.get(ATTR_ASSUMED_STATE) + with pytest.raises(NotValidPresetModeError): + await common.async_turn_on(hass, "fan.test", preset_mode="auto") - await common.async_turn_on(hass, "fan.test", preset_mode="high") + await common.async_turn_on(hass, "fan.test", preset_mode="whoosh") assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -1471,14 +1391,11 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test", percentage=50) - assert mqtt_mock.async_publish.call_count == 4 + assert mqtt_mock.async_publish.call_count == 3 mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "50", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_ON @@ -1501,26 +1418,20 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 33) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "33", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 50) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "50", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_OFF @@ -1529,22 +1440,18 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca await common.async_set_percentage(hass, "fan.test", 100) assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "100", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "freaking-high", 0, False - ) + # use of speeds is deprecated, support will be removed after a quarter (2021.7) + mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_percentage(hass, "fan.test", 0) - assert mqtt_mock.async_publish.call_count == 3 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.async_publish.assert_any_call("percentage-command-topic", "0", 0, False) # use of speeds is deprecated, support will be removed after a quarter (2021.7) mqtt_mock.async_publish.assert_any_call("speed-command-topic", "off", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "off", 0, False - ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") assert state.state == STATE_OFF @@ -1554,32 +1461,16 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca await common.async_set_percentage(hass, "fan.test", 101) await common.async_set_preset_mode(hass, "fan.test", "low") - assert mqtt_mock.async_publish.call_count == 2 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "low", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "low", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid preset mode" in caplog.text + caplog.clear() await common.async_set_preset_mode(hass, "fan.test", "medium") - assert mqtt_mock.async_publish.call_count == 2 - # use of speeds is deprecated, support will be removed after a quarter (2021.7) - mqtt_mock.async_publish.assert_any_call("speed-command-topic", "medium", 0, False) - mqtt_mock.async_publish.assert_any_call( - "preset-mode-command-topic", "medium", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid preset mode" in caplog.text + caplog.clear() - await common.async_set_preset_mode(hass, "fan.test", "high") + await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( - "preset-mode-command-topic", "high", 0, False + "preset-mode-command-topic", "whoosh", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") @@ -1595,7 +1486,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "ModeX") + await common.async_set_preset_mode(hass, "fan.test", "freaking-high") assert "not a valid preset mode" in caplog.text caplog.clear() @@ -1615,13 +1506,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_speed(hass, "fan.test", fan.SPEED_HIGH) - mqtt_mock.async_publish.assert_called_once_with( - "speed-command-topic", "high", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) + assert "not a valid speed" in caplog.text + caplog.clear() await common.async_set_speed(hass, "fan.test", fan.SPEED_OFF) mqtt_mock.async_publish.assert_called_once_with( @@ -1653,7 +1539,7 @@ async def test_attributes(hass, mqtt_mock, caplog): "preset_mode_command_topic": "preset-mode-command-topic", "percentage_command_topic": "percentage-command-topic", "preset_modes": [ - "freaking-high", + "breeze", "silent", ], } @@ -1667,7 +1553,6 @@ async def test_attributes(hass, mqtt_mock, caplog): "low", "medium", "high", - "freaking-high", ] await common.async_turn_on(hass, "fan.test") @@ -1821,14 +1706,14 @@ async def test_supported_features(hass, mqtt_mock): "name": "test3c2", "command_topic": "command-topic", "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["very-fast", "auto"], + "preset_modes": ["eco", "auto"], }, { "platform": "mqtt", "name": "test3c3", "command_topic": "command-topic", "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["off", "on", "auto"], + "preset_modes": ["eco", "smart", "auto"], }, { "platform": "mqtt", @@ -1863,7 +1748,7 @@ async def test_supported_features(hass, mqtt_mock): "name": "test5pr_mb", "command_topic": "command-topic", "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["off", "on", "auto"], + "preset_modes": ["whoosh", "silent", "auto"], }, { "platform": "mqtt", @@ -1927,10 +1812,7 @@ async def test_supported_features(hass, mqtt_mock): assert state is None state = hass.states.get("fan.test3c2") - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_PRESET_MODE | fan.SUPPORT_SET_SPEED - ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE state = hass.states.get("fan.test3c3") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE @@ -1949,21 +1831,19 @@ async def test_supported_features(hass, mqtt_mock): ) state = hass.states.get("fan.test5pr_ma") - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_SET_SPEED | fan.SUPPORT_PRESET_MODE - ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE state = hass.states.get("fan.test5pr_mb") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE state = hass.states.get("fan.test5pr_mc") assert ( state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED | fan.SUPPORT_PRESET_MODE + == fan.SUPPORT_OSCILLATE | fan.SUPPORT_PRESET_MODE ) state = hass.states.get("fan.test6spd_range_a") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED + assert state.attributes.get("percentage_step") == 2.5 state = hass.states.get("fan.test6spd_range_b") assert state is None state = hass.states.get("fan.test6spd_range_c") From 995e22d3bb440f6ff2e17ef66237a063aac1d018 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 10 Apr 2021 02:07:04 +0200 Subject: [PATCH 082/120] Bump devolo Home Control to support old websocket-client versions again (#48960) --- homeassistant/components/devolo_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index 93cf4be5d35..e53e715ffb1 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -2,7 +2,7 @@ "domain": "devolo_home_control", "name": "devolo Home Control", "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", - "requirements": ["devolo-home-control-api==0.17.1"], + "requirements": ["devolo-home-control-api==0.17.3"], "after_dependencies": ["zeroconf"], "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], diff --git a/requirements_all.txt b/requirements_all.txt index 694f7c73b89..26c9af80aa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -479,7 +479,7 @@ deluge-client==1.7.1 denonavr==0.9.10 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.17.1 +devolo-home-control-api==0.17.3 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3936ed66128..0094e499f22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -261,7 +261,7 @@ defusedxml==0.6.0 denonavr==0.9.10 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.17.1 +devolo-home-control-api==0.17.3 # homeassistant.components.directv directv==0.4.0 From 0520ce5ed3bc81f3b72747bb1e74e6f6466d268a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Apr 2021 16:12:40 -0700 Subject: [PATCH 083/120] Fix config forwarding (#48967) --- homeassistant/components/template/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 72a97d6eeab..3b10e708e51 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -68,7 +68,7 @@ async def _process_config(hass, config): async def init_coordinator(hass, conf): coordinator = TriggerUpdateCoordinator(hass, conf) - await coordinator.async_setup(conf) + await coordinator.async_setup(config) return coordinator hass.data[DOMAIN] = await asyncio.gather( From 12da88cae9cec403d79377735330f23e6ac180c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Apr 2021 13:56:15 -1000 Subject: [PATCH 084/120] Prevent ping id allocation conflict with device_tracker (#48969) * Prevent ping id allocation conflict with device_tracker - Solves id conflict resulting unexpected home state * Update homeassistant/components/ping/device_tracker.py Co-authored-by: Paulus Schoutsen --- homeassistant/components/ping/__init__.py | 22 ++++++++------- .../components/ping/device_tracker.py | 2 +- tests/components/ping/test_init.py | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 tests/components/ping/test_init.py diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index 726bb212574..b9a9f6460db 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -24,20 +24,22 @@ async def async_setup(hass, config): @callback -def async_get_next_ping_id(hass): +def async_get_next_ping_id(hass, count=1): """Find the next id to use in the outbound ping. + When using multiping, we increment the id + by the number of ids that multiping + will use. + Must be called in async """ - current_id = hass.data[DOMAIN][PING_ID] - if current_id == MAX_PING_ID: - next_id = DEFAULT_START_ID - else: - next_id = current_id + 1 - - hass.data[DOMAIN][PING_ID] = next_id - - return next_id + allocated_id = hass.data[DOMAIN][PING_ID] + 1 + if allocated_id > MAX_PING_ID: + allocated_id -= MAX_PING_ID - DEFAULT_START_ID + hass.data[DOMAIN][PING_ID] += count + if hass.data[DOMAIN][PING_ID] > MAX_PING_ID: + hass.data[DOMAIN][PING_ID] -= MAX_PING_ID - DEFAULT_START_ID + return allocated_id def _can_use_icmp_lib_with_privilege() -> None | bool: diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index a6b75a9245b..256023263ba 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -125,7 +125,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): count=PING_ATTEMPTS_COUNT, timeout=ICMP_TIMEOUT, privileged=privileged, - id=async_get_next_ping_id(hass), + id=async_get_next_ping_id(hass, len(ip_to_dev_id)), ) ) _LOGGER.debug("Multiping responses: %s", responses) diff --git a/tests/components/ping/test_init.py b/tests/components/ping/test_init.py new file mode 100644 index 00000000000..3dfe193c4d5 --- /dev/null +++ b/tests/components/ping/test_init.py @@ -0,0 +1,27 @@ +"""Test ping id allocation.""" + +from homeassistant.components.ping import async_get_next_ping_id +from homeassistant.components.ping.const import ( + DEFAULT_START_ID, + DOMAIN, + MAX_PING_ID, + PING_ID, +) + + +async def test_async_get_next_ping_id(hass): + """Verify we allocate ping ids as expected.""" + hass.data[DOMAIN] = {PING_ID: DEFAULT_START_ID} + + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 1 + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 2 + assert async_get_next_ping_id(hass, 2) == DEFAULT_START_ID + 3 + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 5 + + hass.data[DOMAIN][PING_ID] = MAX_PING_ID + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 1 + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 2 + + hass.data[DOMAIN][PING_ID] = MAX_PING_ID + assert async_get_next_ping_id(hass, 2) == DEFAULT_START_ID + 1 + assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 3 From 4ca40367d19e494421766e060352b191171e9495 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 10 Apr 2021 02:58:44 +0300 Subject: [PATCH 085/120] Fix Shelly button device triggers (#48974) --- .../components/shelly/device_trigger.py | 16 ++++- .../components/shelly/test_device_trigger.py | 70 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index b7cf1120949..97938040543 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -27,6 +27,7 @@ from .const import ( DOMAIN, EVENT_SHELLY_CLICK, INPUTS_EVENTS_SUBTYPES, + SHBTN_1_INPUTS_EVENTS_TYPES, SUPPORTED_INPUTS_EVENTS_TYPES, ) from .utils import get_device_wrapper, get_input_triggers @@ -45,7 +46,7 @@ async def async_validate_trigger_config(hass, config): # if device is available verify parameters against device capabilities wrapper = get_device_wrapper(hass, config[CONF_DEVICE_ID]) - if not wrapper: + if not wrapper or not wrapper.device.initialized: return config trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -68,6 +69,19 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: if not wrapper: raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") + if wrapper.model in ("SHBTN-1", "SHBTN-2"): + for trigger in SHBTN_1_INPUTS_EVENTS_TYPES: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger, + CONF_SUBTYPE: "button", + } + ) + return triggers + for block in wrapper.device.blocks: input_triggers = get_input_triggers(wrapper.device, block) diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index a725f5a1f30..bedf4abc0f2 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -1,4 +1,6 @@ """The tests for Shelly device triggers.""" +from unittest.mock import AsyncMock, Mock + import pytest from homeassistant import setup @@ -6,10 +8,13 @@ from homeassistant.components import automation from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) +from homeassistant.components.shelly import ShellyDeviceWrapper from homeassistant.components.shelly.const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, + COAP, CONF_SUBTYPE, + DATA_CONFIG_ENTRY, DOMAIN, EVENT_SHELLY_CLICK, ) @@ -52,6 +57,71 @@ async def test_get_triggers(hass, coap_wrapper): assert_lists_same(triggers, expected_triggers) +async def test_get_triggers_button(hass): + """Test we get the expected triggers from a shelly button.""" + await async_setup_component(hass, "shelly", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"sleep_period": 43200, "model": "SHBTN-1"}, + unique_id="12345678", + ) + config_entry.add_to_hass(hass) + + device = Mock( + blocks=None, + settings=None, + shelly=None, + update=AsyncMock(), + initialized=False, + ) + + hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} + hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} + coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ + COAP + ] = ShellyDeviceWrapper(hass, config_entry, device) + + await coap_wrapper.async_setup() + + expected_triggers = [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "single", + CONF_SUBTYPE: "button", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "double", + CONF_SUBTYPE: "button", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "triple", + CONF_SUBTYPE: "button", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "long", + CONF_SUBTYPE: "button", + }, + ] + + triggers = await async_get_device_automations( + hass, "trigger", coap_wrapper.device_id + ) + + assert_lists_same(triggers, expected_triggers) + + async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper): """Test error raised for invalid shelly device_id.""" assert coap_wrapper From 31b061e8f1bb501357335d9fc675a5157fd8c901 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 10 Apr 2021 00:30:34 +0000 Subject: [PATCH 086/120] Bumped version to 2021.4.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ffc2d1a3427..9e6fe036d5c 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 = "2" +PATCH_VERSION = "3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From c08ae640857eead53567621d507e5749ecc99baa Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Fri, 9 Apr 2021 20:36:57 -0700 Subject: [PATCH 087/120] Update python-smarttub to 0.0.23 (#48978) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 2425268e05c..5505ba69a6d 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -6,7 +6,7 @@ "dependencies": [], "codeowners": ["@mdz"], "requirements": [ - "python-smarttub==0.0.19" + "python-smarttub==0.0.23" ], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 26c9af80aa3..349847b9791 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1822,7 +1822,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.19 +python-smarttub==0.0.23 # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0094e499f22..a8c2c3931d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -959,7 +959,7 @@ python-nest==4.1.0 python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.smarttub -python-smarttub==0.0.19 +python-smarttub==0.0.23 # homeassistant.components.songpal python-songpal==0.12 From a9602e7a082ca6428996d7b9a15ed190cb66607e Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Fri, 9 Apr 2021 20:36:57 -0700 Subject: [PATCH 088/120] Update python-smarttub to 0.0.23 (#48978) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 2425268e05c..5505ba69a6d 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -6,7 +6,7 @@ "dependencies": [], "codeowners": ["@mdz"], "requirements": [ - "python-smarttub==0.0.19" + "python-smarttub==0.0.23" ], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 26c9af80aa3..349847b9791 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1822,7 +1822,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.19 +python-smarttub==0.0.23 # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0094e499f22..a8c2c3931d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -959,7 +959,7 @@ python-nest==4.1.0 python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.smarttub -python-smarttub==0.0.19 +python-smarttub==0.0.23 # homeassistant.components.songpal python-songpal==0.12 From 82cca8fb1c640bb60c7f2c4f5790b6d466da8b64 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Apr 2021 15:13:58 +0200 Subject: [PATCH 089/120] Move cast config flow tests to test_config_flow (#48362) --- tests/components/cast/test_config_flow.py | 238 +++++++++++++++++++++ tests/components/cast/test_init.py | 240 +--------------------- 2 files changed, 241 insertions(+), 237 deletions(-) create mode 100644 tests/components/cast/test_config_flow.py diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py new file mode 100644 index 00000000000..064406df717 --- /dev/null +++ b/tests/components/cast/test_config_flow.py @@ -0,0 +1,238 @@ +"""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"} + + # 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/cast/test_init.py b/tests/components/cast/test_init.py index 888ef2ebcd7..178f721959f 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"] - ) From b96e0e69f2df75d2f459cb1ff5cfdc7a6e8bdf49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Apr 2021 00:42:42 -1000 Subject: [PATCH 090/120] Bump nexia to 0.9.6 (#48982) - Now returns None when a humidity sensor cannot be read instead of throwing an exception --- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/util.py | 2 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index cb3493ebc55..253400c886d 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 665aa137065..74272a3c7fd 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/requirements_all.txt b/requirements_all.txt index 349847b9791..7cef102435b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 a8c2c3931d7..3f9a3450956 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From d081ac8d4a3d7d90b2af07800edec1335313ad16 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 10 Apr 2021 20:48:33 +0100 Subject: [PATCH 091/120] Set Lyric hold time to use local time instead of utc (#48994) --- homeassistant/components/lyric/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 0e3672f952e..e57bfd0c514 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 @@ SCHEMA_HOLD_TIME = { 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())), ) } From 0bb7592fab3feddb5290b4459f58d2aa66c450aa Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 13 Apr 2021 01:52:51 +0300 Subject: [PATCH 092/120] Fix Shelly brightness offset (#49007) --- homeassistant/components/shelly/light.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index a9e13796875..370522415fd 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -118,15 +118,16 @@ class ShellyLight(ShellyBlockEntity, LightEntity): """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 @@ class ShellyLight(ShellyBlockEntity, LightEntity): 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)) From 0a5a5ff053ca6b4da7dd8638544f0d1f73c390b7 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 11 Apr 2021 08:42:32 +0200 Subject: [PATCH 093/120] Bump ha-philipsjs to 2.7.0 (#49008) This has some improvements to not consider the TV off due to some exceptions that is related to API being buggy rather than off. --- homeassistant/components/philips_js/__init__.py | 6 +++++- homeassistant/components/philips_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 7be5efeaf2f..b585451cdb0 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -134,8 +134,12 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): 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 ad591ad330b..36e01d8f3c8 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/requirements_all.txt b/requirements_all.txt index 7cef102435b..8507178958e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f9a3450956..a95c1201d0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From e3b3d136d808dc0f1d9075f23a89037ec0efefa0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 10 Apr 2021 22:03:44 +0200 Subject: [PATCH 094/120] Fix use search instead of match to filter logs (#49017) --- homeassistant/components/logger/__init__.py | 2 +- tests/components/logger/test_init.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index fb2920fb6e2..c7660f2a3f0 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/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index d2b0e8931b6..6435ef95394 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 @@ async def test_log_filtering(hass, caplog): 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") From 36e08e770b04aa9f5aab2fc54788fdb6ee63cba6 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 11 Apr 2021 12:35:42 -0500 Subject: [PATCH 095/120] Resolve potential roku setup memory leaks (#49025) * resolve potential roku setup memory leaks * Update __init__.py --- homeassistant/components/roku/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 4a349265459..f8294c878dd 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( From 0d00e49dfca623330281ed13c7ab4f6f7531c0b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Apr 2021 06:42:46 -1000 Subject: [PATCH 096/120] Bump aiohomekit to 0.2.61 (#49044) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 9580a7ee50d..d4e7eb83ee3 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/requirements_all.txt b/requirements_all.txt index 8507178958e..b7443061ee8 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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a95c1201d0c..8e710e3800a 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 From e685b1a1e34611da0fb860a0e7977bb793894bce Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Apr 2021 01:53:44 +0200 Subject: [PATCH 097/120] Fix cast options flow overwriting data (#49051) --- homeassistant/components/cast/config_flow.py | 2 +- tests/components/cast/test_config_flow.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index 464283e07f3..86d85588967 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -133,7 +133,7 @@ class CastOptionsFlowHandler(config_entries.OptionsFlow): ) 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/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 064406df717..1febd9d8803 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -166,6 +166,7 @@ async def test_option_flow(hass, parameter_data): 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} @@ -201,7 +202,12 @@ async def test_option_flow(hass, parameter_data): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] is None - assert config_entry.data == {"ignore_cec": [], "known_hosts": [], "uuid": []} + assert config_entry.data == { + **orig_data, + "ignore_cec": [], + "known_hosts": [], + "uuid": [], + } async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock): From e0131f726f7196e2173fcdd22a641d8e3146ab15 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Apr 2021 18:32:12 +0200 Subject: [PATCH 098/120] Quote media_source paths (#49054) * Quote path in async_sign_path * Address review comments, add tests * Update tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 Co-authored-by: Paulus Schoutsen --- homeassistant/components/cast/media_player.py | 3 ++- homeassistant/components/http/auth.py | 10 ++++++++-- homeassistant/components/media_source/__init__.py | 3 ++- tests/components/media_source/test_init.py | 12 +++++++----- tests/components/media_source/test_local_source.py | 3 +++ tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 | 1 + 6 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 016d5162d23..afd6065cb98 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -7,6 +7,7 @@ from datetime import timedelta 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 @@ class CastDevice(MediaPlayerEntity): 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/http/auth.py b/homeassistant/components/http/auth.py index 3267c9cc70e..38275819483 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/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 0ef5d460580..5b027a99bf9 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/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 0dda9f67fbe..d8ee73ebc2f 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 e3e2a3f1617..aff4f92be02 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/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 00000000000..23bd6ccc564 --- /dev/null +++ b/tests/testing_config/media/Epic Sax Guy 10 Hours.mp4 @@ -0,0 +1 @@ +I play the sax From 21b55515063fa288d2453e1c9b0f16ee16acf04f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 12 Apr 2021 01:53:07 +0200 Subject: [PATCH 099/120] mqtt fan percentage to speed_range and received speed_state fix (#49060) * percentage to speed_range and get speed state fix * Update homeassistant/components/mqtt/fan.py * Update homeassistant/components/mqtt/fan.py * Update homeassistant/components/mqtt/fan.py * Update homeassistant/components/mqtt/fan.py Co-authored-by: J. Nick Koston --- homeassistant/components/mqtt/fan.py | 16 ++++++++-------- tests/components/mqtt/test_fan.py | 20 ++++++++++++++++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 6009b941c5c..24c4c805dfd 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 @@ class MqttFan(MqttEntity, FanEntity): ) 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 @@ class MqttFan(MqttEntity, FanEntity): 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/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 5caec9b7473..bfa1f387bcd 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") From 4eb794ae84df9b073697c6c9ca55a86a164f618b Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 11 Apr 2021 15:12:59 -0700 Subject: [PATCH 100/120] Catch unknown equipment values (#49073) * Catch unknown equipment values * Catch unknown equipment values * Remove warning spam. --- homeassistant/components/screenlogic/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 11f71cfd337..c5c082cd509 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -195,9 +195,15 @@ class ScreenlogicEntity(CoordinatorEntity): """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, } From bf282687323b68dd56034f9bae3e79d0de3cbc42 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Apr 2021 10:36:26 -1000 Subject: [PATCH 101/120] Downgrade logger message about homekit id missing (#49079) This can happen if the TXT record is received after the PTR record and should not generate a warning since it will get processed later --- homeassistant/components/homekit_controller/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index fcf83918fda..4a3deee4d11 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -209,8 +209,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): } 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") From b5650bdd526461684fd530281a6e24dafae5b123 Mon Sep 17 00:00:00 2001 From: Unai Date: Tue, 13 Apr 2021 00:14:29 +0200 Subject: [PATCH 102/120] Upgrade maxcube-api to 0.4.2 (#49106) Upgrade to maxcube-api 0.4.2 to fix pending issues in HA 2021.4.x: - Interpret correctly S command error responses (https://github.com/home-assistant/core/issues/49075) - Support application timezone configuration (https://github.com/home-assistant/core/issues/49076) --- homeassistant/components/maxcube/__init__.py | 3 ++- homeassistant/components/maxcube/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/maxcube/conftest.py | 3 ++- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index e38f08809a7..4d610dfc04f 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -10,6 +10,7 @@ import voluptuous as vol 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 ddc21bd2358..75b5a5fcb6d 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/requirements_all.txt b/requirements_all.txt index b7443061ee8..4831b310f8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e710e3800a..33ef9a95aa8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/maxcube/conftest.py b/tests/components/maxcube/conftest.py index 6b283cf87c0..b36072190c4 100644 --- a/tests/components/maxcube/conftest.py +++ b/tests/components/maxcube/conftest.py @@ -10,6 +10,7 @@ import pytest 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 From 346ae78a8e168ebe948f265874c06f70ef489a1f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 12 Apr 2021 11:29:45 -0500 Subject: [PATCH 103/120] Check all endpoints for zwave_js.climate hvac_action (#49115) --- homeassistant/components/zwave_js/climate.py | 1 + tests/components/zwave_js/test_climate.py | 2 ++ tests/components/zwave_js/test_services.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index c64a5ef788f..41ea873c5f8 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -150,6 +150,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): 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/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 83a607f3add..2084e771546 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -12,6 +12,7 @@ from homeassistant.components.climate.const import ( 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 7bdba7894d2..956361d3953 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): From e5281051a3012bf1033ca1ed5b1865ec054e8994 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 13 Apr 2021 00:10:42 +0000 Subject: [PATCH 104/120] Bumped version to 2021.4.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9e6fe036d5c..875e60e2202 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) From 3ae82c3cacd5715d6b579e1aca4171e8ab5758b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 00:31:55 -1000 Subject: [PATCH 105/120] Bump aiodiscover to 1.3.4 (#49142) - Changelog: https://github.com/bdraco/aiodiscover/compare/v1.3.3...v1.3.4 (bumps pyroute2>=0.5.18 to fix https://github.com/svinota/pyroute2/issues/717) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 80cc6b116c9..e841fb6bebb 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -3,7 +3,7 @@ "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", "requirements": [ - "scapy==2.4.4", "aiodiscover==1.3.3" + "scapy==2.4.4", "aiodiscover==1.3.4" ], "codeowners": [ "@bdraco" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c5f0ccde05d..313c2af3a70 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiodiscover==1.3.3 +aiodiscover==1.3.4 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==1.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4831b310f8e..ca36481bfed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioazuredevops==1.3.5 aiobotocore==0.11.1 # homeassistant.components.dhcp -aiodiscover==1.3.3 +aiodiscover==1.3.4 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33ef9a95aa8..5ee10757f4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ aioazuredevops==1.3.5 aiobotocore==0.11.1 # homeassistant.components.dhcp -aiodiscover==1.3.3 +aiodiscover==1.3.4 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 02def469914d94c5a0c8fb61af951d0cec02c13a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 00:44:07 -1000 Subject: [PATCH 106/120] Fix setting up remotes that lack a supported features list in homekit (#49152) --- homeassistant/components/homekit/util.py | 2 +- tests/components/homekit/test_config_flow.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index a746355e124..673abc5da67 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -507,5 +507,5 @@ def state_needs_accessory_mode(state): or state.domain == MEDIA_PLAYER_DOMAIN and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV or state.domain == REMOTE_DOMAIN - and state.attributes.get(ATTR_SUPPORTED_FEATURES) & SUPPORT_ACTIVITY + and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & SUPPORT_ACTIVITY ) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 26804675265..c06e8aaa5ad 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -145,6 +145,8 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): hass.states.async_set("camera.one", "on") hass.states.async_set("camera.existing", "on") hass.states.async_set("media_player.two", "on", {"device_class": "tv"}) + hass.states.async_set("remote.standard", "on") + hass.states.async_set("remote.activity", "on", {"supported_features": 4}) bridge_mode_entry = MockConfigEntry( domain=DOMAIN, @@ -178,7 +180,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"include_domains": ["camera", "media_player", "light"]}, + {"include_domains": ["camera", "media_player", "light", "remote"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "pairing" @@ -205,7 +207,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): "filter": { "exclude_domains": [], "exclude_entities": [], - "include_domains": ["media_player", "light"], + "include_domains": ["media_player", "light", "remote"], "include_entities": [], }, "exclude_accessory_mode": True, @@ -222,7 +224,8 @@ async def test_setup_creates_entries_for_accessory_mode_devices(hass): # 3 - new bridge # 4 - camera.one in accessory mode # 5 - media_player.two in accessory mode - assert len(mock_setup_entry.mock_calls) == 5 + # 6 - remote.activity in accessory mode + assert len(mock_setup_entry.mock_calls) == 6 async def test_import(hass): From 5356ce2b6d7352b652139c68689846f77c11e966 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Apr 2021 15:09:50 +0200 Subject: [PATCH 107/120] Don't receive homeassistant_* events from MQTT eventstream (#49158) --- .../components/mqtt_eventstream/__init__.py | 22 +++++++++++-- .../components/mqtt_eventstream/test_init.py | 31 ++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index 328b9395eea..d31d6d1cd53 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -7,6 +7,11 @@ from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_t from homeassistant.const import ( ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, @@ -37,6 +42,14 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +BLOCKED_EVENTS = [ + EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_FINAL_WRITE, +] + async def async_setup(hass, config): """Set up the MQTT eventstream component.""" @@ -45,16 +58,15 @@ async def async_setup(hass, config): pub_topic = conf.get(CONF_PUBLISH_TOPIC) sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) ignore_event = conf.get(CONF_IGNORE_EVENT) + ignore_event.append(EVENT_TIME_CHANGED) @callback def _event_publisher(event): """Handle events by publishing them on the MQTT queue.""" if event.origin != EventOrigin.local: return - if event.event_type == EVENT_TIME_CHANGED: - return - # User-defined events to ignore + # Events to ignore if event.event_type in ignore_event: return @@ -84,6 +96,10 @@ async def async_setup(hass, config): event_type = event.get("event_type") event_data = event.get("event_data") + # Don't fire HOMEASSISTANT_* events on this instance + if event_type in BLOCKED_EVENTS: + return + # Special case handling for event STATE_CHANGED # We will try to convert state dicts back to State objects # Copied over from the _handle_api_post_events_event method diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index 7f6b22bda90..6a1633cb111 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -3,7 +3,7 @@ import json from unittest.mock import ANY, patch import homeassistant.components.mqtt_eventstream as eventstream -from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL from homeassistant.core import State, callback from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component @@ -114,6 +114,7 @@ async def test_time_event_does_not_send_message(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() async_fire_time_changed(hass, dt_util.utcnow()) + await hass.async_block_till_done() assert not mqtt_mock.async_publish.called @@ -140,6 +141,33 @@ async def test_receiving_remote_event_fires_hass_event(hass, mqtt_mock): assert len(calls) == 1 + await hass.async_block_till_done() + + +async def test_receiving_blocked_event_fires_hass_event(hass, mqtt_mock): + """Test the receiving of blocked event does not fire.""" + sub_topic = "foo" + assert await add_eventstream(hass, sub_topic=sub_topic) + await hass.async_block_till_done() + + calls = [] + + @callback + def listener(_): + calls.append(1) + + hass.bus.async_listen(MATCH_ALL, listener) + await hass.async_block_till_done() + + for event in eventstream.BLOCKED_EVENTS: + payload = json.dumps({"event_type": event, "event_data": {}}, cls=JSONEncoder) + async_fire_mqtt_message(hass, sub_topic, payload) + await hass.async_block_till_done() + + assert len(calls) == 0 + + await hass.async_block_till_done() + async def test_ignored_event_doesnt_send_over_stream(hass, mqtt_mock): """Test the ignoring of sending events if defined.""" @@ -159,6 +187,7 @@ async def test_ignored_event_doesnt_send_over_stream(hass, mqtt_mock): # Set a state of an entity mock_state_change_event(hass, State(e_id, "on")) await hass.async_block_till_done() + await hass.async_block_till_done() assert not mqtt_mock.async_publish.called From a3ca48c1bda811330a527e8f108c05751d185e84 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Apr 2021 17:24:21 +0200 Subject: [PATCH 108/120] Set deprecated supported_features for MQTT JSON light (#49167) * Set deprecated supported_features for MQTT json light * Update homeassistant/components/light/__init__.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/light/__init__.py | 17 +++++ .../components/mqtt/light/schema_json.py | 5 +- tests/components/mqtt/test_light_json.py | 71 +++++++++++++++---- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index fe9a38d12b4..bfdb723e159 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -751,3 +751,20 @@ class Light(LightEntity): "Light is deprecated, modify %s to extend LightEntity", cls.__name__, ) + + +def legacy_supported_features( + supported_features: int, supported_color_modes: list[str] | None +) -> int: + """Calculate supported features with backwards compatibility.""" + # Backwards compatibility for supported_color_modes added in 2021.4 + if supported_color_modes is None: + return supported_features + if any(mode in supported_color_modes for mode in COLOR_MODES_COLOR): + supported_features |= SUPPORT_COLOR + if any(mode in supported_color_modes for mode in COLOR_MODES_BRIGHTNESS): + supported_features |= SUPPORT_BRIGHTNESS + if COLOR_MODE_COLOR_TEMP in supported_color_modes: + supported_features |= SUPPORT_COLOR_TEMP + + return supported_features diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index aaf12f3362f..9940d646a35 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -35,6 +35,7 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, VALID_COLOR_MODES, LightEntity, + legacy_supported_features, valid_supported_color_modes, ) from homeassistant.const import ( @@ -458,7 +459,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): @property def supported_features(self): """Flag supported features.""" - return self._supported_features + return legacy_supported_features( + self._supported_features, self._config.get(CONF_SUPPORTED_COLOR_MODES) + ) def _set_flash_and_transition(self, message, **kwargs): if ATTR_TRANSITION in kwargs: diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 6c9c7ae903a..77e5936c7b4 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -234,10 +234,10 @@ async def test_rgb_light(hass, mqtt_mock): state = hass.states.get("light.test") expected_features = ( - light.SUPPORT_TRANSITION + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_FLASH - | light.SUPPORT_BRIGHTNESS + | light.SUPPORT_TRANSITION ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features @@ -261,7 +261,8 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -310,7 +311,16 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 191 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -429,7 +439,15 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("brightness") is None assert state.attributes.get("color_mode") is None assert state.attributes.get("color_temp") is None @@ -610,7 +628,16 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get("effect") == "random" assert state.attributes.get("color_temp") == 100 assert state.attributes.get("white_value") == 50 - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 191 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "light.test") @@ -738,7 +765,15 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_EFFECT + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("brightness") == 95 assert state.attributes.get("color_mode") == "rgb" assert state.attributes.get("color_temp") is None @@ -1313,7 +1348,10 @@ async def test_effect(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + expected_features = ( + light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test") @@ -1373,7 +1411,8 @@ async def test_flash_short_and_long(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test", flash="short") @@ -1431,8 +1470,8 @@ async def test_transition(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 - + expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test", transition=15) mqtt_mock.async_publish.assert_called_once_with( @@ -1523,7 +1562,15 @@ async def test_invalid_values(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 187 + expected_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_FLASH + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("white_value") is None From 5c3cb044d03414d757f95e256c91e00e72e0b412 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Apr 2021 23:52:10 +0200 Subject: [PATCH 109/120] Upgrade spotipy to 2.18.0 (#49220) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index bd92217e9cf..d0d40291fff 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.17.1"], + "requirements": ["spotipy==2.18.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index ca36481bfed..df61a7be783 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2117,7 +2117,7 @@ spiderpy==1.4.2 spotcrime==1.0.4 # homeassistant.components.spotify -spotipy==2.17.1 +spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ee10757f4a..5c2fdb06cb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1104,7 +1104,7 @@ speedtest-cli==2.1.3 spiderpy==1.4.2 # homeassistant.components.spotify -spotipy==2.17.1 +spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql From ba0d3aad1c9a413f1d58933d8b8f00de724f24b3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Apr 2021 09:03:34 +0200 Subject: [PATCH 110/120] Fix race when restarting script (#49247) --- homeassistant/helpers/script.py | 25 +++++++++++++------ tests/components/automation/test_blueprint.py | 12 ++++----- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index bf52fc81b6a..52be3866639 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1144,10 +1144,7 @@ class Script: self._log("Already running", level=LOGSEVERITY[self._max_exceeded]) script_execution_set("failed_single") return - if self.script_mode == SCRIPT_MODE_RESTART: - self._log("Restarting") - await self.async_stop(update_state=False) - elif len(self._runs) == self.max_runs: + if self.script_mode != SCRIPT_MODE_RESTART and self.runs == self.max_runs: if self._max_exceeded != "SILENT": self._log( "Maximum number of runs exceeded", @@ -1186,6 +1183,14 @@ class Script: self._hass, self, cast(dict, variables), context, self._log_exceptions ) self._runs.append(run) + if self.script_mode == SCRIPT_MODE_RESTART: + # When script mode is SCRIPT_MODE_RESTART, first add the new run and then + # stop any other runs. If we stop other runs first, self.is_running will + # return false after the other script runs were stopped until our task + # resumes running. + self._log("Restarting") + await self.async_stop(update_state=False, spare=run) + if started_action: self._hass.async_run_job(started_action) self.last_triggered = utcnow() @@ -1198,17 +1203,21 @@ class Script: self._changed() raise - async def _async_stop(self, update_state): - aws = [asyncio.create_task(run.async_stop()) for run in self._runs] + async def _async_stop(self, update_state, spare=None): + aws = [ + asyncio.create_task(run.async_stop()) for run in self._runs if run != spare + ] if not aws: return await asyncio.wait(aws) if update_state: self._changed() - async def async_stop(self, update_state: bool = True) -> None: + async def async_stop( + self, update_state: bool = True, spare: _ScriptRun | None = None + ) -> None: """Stop running script.""" - await asyncio.shield(self._async_stop(update_state)) + await asyncio.shield(self._async_stop(update_state, spare)) async def _async_get_condition(self, config): if isinstance(config, template.Template): diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 747e162fe46..e035c238383 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -156,8 +156,8 @@ async def test_motion_light(hass): # Turn on motion hass.states.async_set("binary_sensor.kitchen", "on") # Can't block till done because delay is active - # So wait 5 event loop iterations to process script - for _ in range(5): + # So wait 10 event loop iterations to process script + for _ in range(10): await asyncio.sleep(0) assert len(turn_on_calls) == 1 @@ -165,7 +165,7 @@ async def test_motion_light(hass): # Test light doesn't turn off if motion stays async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) assert len(turn_off_calls) == 0 @@ -173,7 +173,7 @@ async def test_motion_light(hass): # Test light turns off off 120s after last motion hass.states.async_set("binary_sensor.kitchen", "off") - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120)) @@ -184,7 +184,7 @@ async def test_motion_light(hass): # Test restarting the script hass.states.async_set("binary_sensor.kitchen", "on") - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) assert len(turn_on_calls) == 2 @@ -192,7 +192,7 @@ async def test_motion_light(hass): hass.states.async_set("binary_sensor.kitchen", "off") - for _ in range(5): + for _ in range(10): await asyncio.sleep(0) hass.states.async_set("binary_sensor.kitchen", "on") From 733a394c554b0d29f88a033bbd4e8f4b7c653068 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 15 Apr 2021 16:12:49 +0200 Subject: [PATCH 111/120] Fix mysensors sensor protocol version check (#49257) --- homeassistant/components/mysensors/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index a62318aea53..1a5f7330ddf 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,6 +1,8 @@ """Support for MySensors sensors.""" from typing import Callable +from awesomeversion import AwesomeVersion + from homeassistant.components import mysensors from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY @@ -115,7 +117,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): """Return the unit of measurement of this entity.""" set_req = self.gateway.const.SetReq if ( - float(self.gateway.protocol_version) >= 1.5 + AwesomeVersion(self.gateway.protocol_version) >= AwesomeVersion("1.5") and set_req.V_UNIT_PREFIX in self._values ): return self._values[set_req.V_UNIT_PREFIX] From 41736c93a1fd46605d70f8cb5db23c5028d138b9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 10:53:19 +0200 Subject: [PATCH 112/120] Fix Coronavirus integration robustness (#49287) Co-authored-by: Martin Hjelmare --- .../components/coronavirus/__init__.py | 18 ++++++++----- .../components/coronavirus/config_flow.py | 13 ++++++++-- .../components/coronavirus/strings.json | 1 + .../coronavirus/translations/en.json | 3 ++- .../coronavirus/test_config_flow.py | 26 ++++++++++++++++++- tests/components/coronavirus/test_init.py | 25 +++++++++++++++++- 6 files changed, 74 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index d05c4cef862..4bda4edcd37 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -15,14 +15,14 @@ from .const import DOMAIN PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the Coronavirus component.""" # Make sure coordinator is initialized. await get_coordinator(hass) return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Coronavirus from a config entry.""" if isinstance(entry.data["country"], int): hass.config_entries.async_update_entry( @@ -44,6 +44,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=entry.data["country"]) + coordinator = await get_coordinator(hass) + if not coordinator.last_update_success: + await coordinator.async_config_entry_first_refresh() + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) @@ -52,9 +56,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( + return all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, platform) @@ -63,10 +67,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) - return unload_ok - -async def get_coordinator(hass): +async def get_coordinator( + hass: HomeAssistant, +) -> update_coordinator.DataUpdateCoordinator: """Get the data update coordinator.""" if DOMAIN in hass.data: return hass.data[DOMAIN] diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index 6d2776c7ecc..4f6e865fa37 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -1,4 +1,8 @@ """Config flow for Coronavirus integration.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -15,13 +19,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _options = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle the initial step.""" errors = {} if self._options is None: - self._options = {OPTION_WORLDWIDE: "Worldwide"} coordinator = await get_coordinator(self.hass) + if not coordinator.last_update_success: + return self.async_abort(reason="cannot_connect") + + self._options = {OPTION_WORLDWIDE: "Worldwide"} for case in sorted( coordinator.data.values(), key=lambda case: case.country ): diff --git a/homeassistant/components/coronavirus/strings.json b/homeassistant/components/coronavirus/strings.json index 6a5b2626003..e0b29d6c8db 100644 --- a/homeassistant/components/coronavirus/strings.json +++ b/homeassistant/components/coronavirus/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } diff --git a/homeassistant/components/coronavirus/translations/en.json b/homeassistant/components/coronavirus/translations/en.json index cbd057bfce1..ea7ba1f6f9d 100644 --- a/homeassistant/components/coronavirus/translations/en.json +++ b/homeassistant/components/coronavirus/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "cannot_connect": "Failed to connect" }, "step": { "user": { diff --git a/tests/components/coronavirus/test_config_flow.py b/tests/components/coronavirus/test_config_flow.py index 06d586ba2a5..bfc69200893 100644 --- a/tests/components/coronavirus/test_config_flow.py +++ b/tests/components/coronavirus/test_config_flow.py @@ -1,9 +1,14 @@ """Test the Coronavirus config flow.""" +from unittest.mock import MagicMock, patch + +from aiohttp import ClientError + from homeassistant import config_entries, setup from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE +from homeassistant.core import HomeAssistant -async def test_form(hass): +async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -24,3 +29,22 @@ async def test_form(hass): } await hass.async_block_till_done() assert len(hass.states.async_all()) == 4 + + +@patch( + "coronavirus.get_cases", + side_effect=ClientError, +) +async def test_abort_on_connection_error( + mock_get_cases: MagicMock, hass: HomeAssistant +) -> None: + """Test we abort on connection error.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert "type" in result + assert result["type"] == "abort" + assert "reason" in result + assert result["reason"] == "cannot_connect" diff --git a/tests/components/coronavirus/test_init.py b/tests/components/coronavirus/test_init.py index cc49bf7d4b6..c36255db9d1 100644 --- a/tests/components/coronavirus/test_init.py +++ b/tests/components/coronavirus/test_init.py @@ -1,12 +1,18 @@ """Test init of Coronavirus integration.""" +from unittest.mock import MagicMock, patch + +from aiohttp import ClientError + from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_registry -async def test_migration(hass): +async def test_migration(hass: HomeAssistant) -> None: """Test that we can migrate coronavirus to stable unique ID.""" nl_entry = MockConfigEntry(domain=DOMAIN, title="Netherlands", data={"country": 34}) nl_entry.add_to_hass(hass) @@ -47,3 +53,20 @@ async def test_migration(hass): assert nl_entry.unique_id == "Netherlands" assert worldwide_entry.unique_id == OPTION_WORLDWIDE + + +@patch( + "coronavirus.get_cases", + side_effect=ClientError, +) +async def test_config_entry_not_ready( + mock_get_cases: MagicMock, hass: HomeAssistant +) -> None: + """Test the configuration entry not ready.""" + entry = MockConfigEntry(domain=DOMAIN, title="Netherlands", data={"country": 34}) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_SETUP_RETRY From 58b743eec1e8ad12b3fcf8d14607410b276565e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 16 Apr 2021 15:00:21 +0200 Subject: [PATCH 113/120] Mark camera as a base platform (#49297) --- homeassistant/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 1b48efb8c0f..778f3ea9b55 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -22,6 +22,7 @@ BASE_PLATFORMS = { "air_quality", "alarm_control_panel", "binary_sensor", + "camera", "climate", "cover", "device_tracker", From a08df4e18f0a8b65978fd8fd526586bfe97a6a02 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Apr 2021 15:35:06 +0200 Subject: [PATCH 114/120] Bumped version to 2021.4.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 875e60e2202..9ebf7516628 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 = "4" +PATCH_VERSION = "5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) From fe2f4e27905a2003a6a9b884672b6d474345e1b4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 16 Apr 2021 22:33:58 +0200 Subject: [PATCH 115/120] Apply Precision/Scale/Offset to struct in modbus sensor (#48544) The single values in struct are corrected with presicion, scale and offset, just as it is done with single values. --- homeassistant/components/modbus/sensor.py | 14 ++++++- tests/components/modbus/test_modbus_sensor.py | 41 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 7aa08070d67..b2b8e27b8c8 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -319,7 +319,19 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): # If unpack() returns a tuple greater than 1, don't try to process the value. # Instead, return the values of unpack(...) separated by commas. if len(val) > 1: - self._value = ",".join(map(str, val)) + # Apply scale and precision to floats and ints + v_result = [] + for entry in val: + v_temp = self._scale * entry + self._offset + + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(v_temp, int) and self._precision == 0: + v_result.append(str(v_temp)) + else: + v_result.append(f"{float(v_temp):.{self._precision}f}") + self._value = ",".join(map(str, v_result)) else: val = val[0] diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index dd485e59835..516979b22d2 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.modbus.const import ( CONF_REVERSE_ORDER, CONF_SCALE, CONF_SENSORS, + DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, DATA_TYPE_STRING, @@ -26,6 +27,7 @@ from homeassistant.const import ( CONF_NAME, CONF_OFFSET, CONF_SLAVE, + CONF_STRUCTURE, ) from .conftest import base_config_test, base_test @@ -338,6 +340,7 @@ async def test_config_sensor(hass, do_discovery, do_config): ) async def test_all_sensor(hass, cfg, regs, expected): """Run test for sensor.""" + sensor_name = "modbus_test_sensor" state = await base_test( hass, @@ -352,3 +355,41 @@ async def test_all_sensor(hass, cfg, regs, expected): scan_interval=5, ) assert state == expected + + +async def test_struct_sensor(hass): + """Run test for sensor struct.""" + + sensor_name = "modbus_test_sensor" + # floats: 7.931250095367432, 10.600000381469727, + # 1.000879611487865e-28, 10.566553115844727 + expected = "7.93,10.60,0.00,10.57" + state = await base_test( + hass, + { + CONF_NAME: sensor_name, + CONF_REGISTER: 1234, + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">4f", + }, + sensor_name, + SENSOR_DOMAIN, + CONF_SENSORS, + CONF_REGISTERS, + [ + 0x40FD, + 0xCCCD, + 0x4129, + 0x999A, + 0x10FD, + 0xC0CD, + 0x4129, + 0x109A, + ], + expected, + method_discovery=False, + scan_interval=5, + ) + assert state == expected From fc4c49ab8319b9e36dded5b47738bcf816c24c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 16 Apr 2021 20:17:46 +0200 Subject: [PATCH 116/120] Upgrade pyMetno to 0.8.2 (#49308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/met/manifest.json | 2 +- homeassistant/components/norway_air/air_quality.py | 2 +- homeassistant/components/norway_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 38b77a0afd2..2724818ad49 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -3,6 +3,6 @@ "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": ["pyMetno==0.8.1"], + "requirements": ["pyMetno==0.8.2"], "codeowners": ["@danielhiversen", "@thimic"] } diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 788f900ef70..480121846e9 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -67,7 +67,7 @@ def round_state(func): class AirSensor(AirQualityEntity): - """Representation of an Yr.no sensor.""" + """Representation of an air quality sensor.""" def __init__(self, name, coordinates, forecast, session): """Initialize the sensor.""" diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 193d96e2a18..5306fa8e3e6 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,6 +2,6 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.8.1"], + "requirements": ["pyMetno==0.8.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index df61a7be783..704ac3fdf01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1229,7 +1229,7 @@ pyHS100==0.3.5.2 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.1 +pyMetno==0.8.2 # homeassistant.components.rfxtrx pyRFXtrx==0.26.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c2fdb06cb0..4808f3c7c2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ pyHS100==0.3.5.2 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.1 +pyMetno==0.8.2 # homeassistant.components.rfxtrx pyRFXtrx==0.26.1 From 7ed8f00075a9fcd6664f311e1a5ea890e2b16ffb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Apr 2021 21:48:02 -1000 Subject: [PATCH 117/120] Fix exception in roomba discovery when the device does not respond on the first try (#49360) --- .../components/roomba/config_flow.py | 5 +- tests/components/roomba/test_config_flow.py | 64 ++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 45c2d8b9a1b..92d9ff05dc0 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -328,9 +328,8 @@ async def _async_discover_roombas(hass, host): discovery = _async_get_roomba_discovery() try: if host: - discovered = [ - await hass.async_add_executor_job(discovery.get, host) - ] + device = await hass.async_add_executor_job(discovery.get, host) + discovered = [device] if device else [] else: discovered = await hass.async_add_executor_job(discovery.get_all) except OSError: diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index ee3b7d4b497..a15ad7e43a6 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -711,7 +711,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): @pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP) async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): - """Test we can process the discovery from dhcp but roomba discovery cannot find the device.""" + """Test we can process the discovery from dhcp but roomba discovery cannot find the specific device.""" await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( @@ -782,6 +782,68 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP) +async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_data): + """Test we can process the discovery from dhcp but roomba discovery cannot find any devices.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_no_devices_found_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=discovery_data, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "myroomba" + assert result3["result"].unique_id == "BLID" + assert result3["data"] == { + CONF_BLID: "BLID", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_dhcp_discovery_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" await setup.async_setup_component(hass, "persistent_notification", {}) From a5a62154d4f53f42e116cc9bc722302798b1b326 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 Apr 2021 14:45:01 +0200 Subject: [PATCH 118/120] Fix deadlock when restarting scripts (#49410) --- homeassistant/helpers/script.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 52be3866639..f2afe152569 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1203,12 +1203,9 @@ class Script: self._changed() raise - async def _async_stop(self, update_state, spare=None): - aws = [ - asyncio.create_task(run.async_stop()) for run in self._runs if run != spare - ] - if not aws: - return + async def _async_stop( + self, aws: list[asyncio.Task], update_state: bool, spare: _ScriptRun | None + ) -> None: await asyncio.wait(aws) if update_state: self._changed() @@ -1217,7 +1214,15 @@ class Script: self, update_state: bool = True, spare: _ScriptRun | None = None ) -> None: """Stop running script.""" - await asyncio.shield(self._async_stop(update_state, spare)) + # Collect a a list of script runs to stop. This must be done before calling + # asyncio.shield as asyncio.shield yields to the event loop, which would cause + # us to wait for script runs added after the call to async_stop. + aws = [ + asyncio.create_task(run.async_stop()) for run in self._runs if run != spare + ] + if not aws: + return + await asyncio.shield(self._async_stop(aws, update_state, spare)) async def _async_get_condition(self, config): if isinstance(config, template.Template): From d61281b6fb7d1c76aa0563d7987c189116f38f19 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 19 Apr 2021 17:20:00 +0200 Subject: [PATCH 119/120] Google report state: thermostatMode should be a string, not null (#49342) --- homeassistant/components/google_assistant/trait.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 384c5bfd0ae..63f76e1d6ea 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -791,7 +791,7 @@ class TemperatureSettingTrait(_Trait): if preset in self.preset_to_google: response["thermostatMode"] = self.preset_to_google[preset] else: - response["thermostatMode"] = self.hvac_to_google.get(operation) + response["thermostatMode"] = self.hvac_to_google.get(operation, "none") current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) if current_temp is not None: From 44744dc0bc7940c70efbe99e07a5e06f4305efa4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Apr 2021 17:32:43 +0200 Subject: [PATCH 120/120] Bumped version to 2021.4.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9ebf7516628..472a5401c6a 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 = "5" +PATCH_VERSION = "6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0)