From 5ef6f87ab988f06995c018a788f3022c4be21419 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 8 Jan 2021 11:50:02 +0100 Subject: [PATCH 01/28] Fix KNX cover state return open when unknown (#44926) --- homeassistant/components/knx/cover.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index b88b1cfe86a..33da600976e 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -79,6 +79,9 @@ class KNXCover(KnxEntity, CoverEntity): @property def is_closed(self): """Return if the cover is closed.""" + # state shall be "unknown" when xknx travelcalculator is not initialized + if self._device.current_position() is None: + return None return self._device.is_closed() @property From 5dfe8e15e3a0e1567445dae16cf905666fa47b6e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 8 Jan 2021 02:07:50 -1000 Subject: [PATCH 02/28] Fix wait_template incorrectly matching falsey values (#44938) --- homeassistant/helpers/script.py | 15 ++----- tests/helpers/test_script.py | 77 +++++++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 77c842a27fe..48a662e3a81 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -62,11 +62,7 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import condition, config_validation as cv, service, template -from homeassistant.helpers.event import ( - TrackTemplate, - async_call_later, - async_track_template_result, -) +from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.trigger import ( async_initialize_triggers, @@ -359,7 +355,7 @@ class _ScriptRun: return @callback - def _async_script_wait(event, updates): + def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" self._variables["wait"] = { "remaining": to_context.remaining if to_context else delay, @@ -368,12 +364,9 @@ class _ScriptRun: done.set() to_context = None - info = async_track_template_result( - self._hass, - [TrackTemplate(wait_template, self._variables)], - _async_script_wait, + unsub = async_track_template( + self._hass, wait_template, async_script_wait, self._variables ) - unsub = info.async_remove self._changed() done = asyncio.Event() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index c81ed681d42..72f4b06d91c 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -7,6 +7,7 @@ import logging from types import MappingProxyType from unittest import mock +from async_timeout import timeout import pytest import voluptuous as vol @@ -544,6 +545,41 @@ async def test_wait_basic(hass, action_type): assert script_obj.last_action is None +@pytest.mark.parametrize("action_type", ["template", "trigger"]) +async def test_wait_basic_times_out(hass, action_type): + """Test wait actions times out when the action does not happen.""" + wait_alias = "wait step" + action = {"alias": wait_alias} + if action_type == "template": + action["wait_template"] = "{{ states.switch.test.state == 'off' }}" + else: + action["wait_for_trigger"] = { + "platform": "state", + "entity_id": "switch.test", + "to": "off", + } + sequence = cv.SCRIPT_SCHEMA(action) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, wait_alias) + timed_out = False + + try: + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(wait_started_flag.wait(), 1) + assert script_obj.is_running + assert script_obj.last_action == wait_alias + hass.states.async_set("switch.test", "not_on") + + with timeout(0.1): + await hass.async_block_till_done() + except asyncio.TimeoutError: + timed_out = True + await script_obj.async_stop() + + assert timed_out + + @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_multiple_runs_wait(hass, action_type): """Test multiple runs with wait in script.""" @@ -782,30 +818,53 @@ async def test_wait_template_variables_in(hass): async def test_wait_template_with_utcnow(hass): """Test the wait template with utcnow.""" - sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ utcnow().hours == 12 }}"}) + sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ utcnow().hour == 12 }}"}) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") - start_time = dt_util.utcnow() + timedelta(hours=24) + start_time = dt_util.utcnow().replace(minute=1) + timedelta(hours=48) try: hass.async_create_task(script_obj.async_run(context=Context())) - async_fire_time_changed(hass, start_time.replace(hour=5)) - assert not script_obj.is_running - async_fire_time_changed(hass, start_time.replace(hour=12)) - await asyncio.wait_for(wait_started_flag.wait(), 1) - assert script_obj.is_running + + match_time = start_time.replace(hour=12) + with patch("homeassistant.util.dt.utcnow", return_value=match_time): + async_fire_time_changed(hass, match_time) except (AssertionError, asyncio.TimeoutError): await script_obj.async_stop() raise else: - async_fire_time_changed(hass, start_time.replace(hour=3)) await hass.async_block_till_done() - assert not script_obj.is_running +async def test_wait_template_with_utcnow_no_match(hass): + """Test the wait template with utcnow that does not match.""" + sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ utcnow().hour == 12 }}"}) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + start_time = dt_util.utcnow().replace(minute=1) + timedelta(hours=48) + timed_out = False + + try: + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(wait_started_flag.wait(), 1) + assert script_obj.is_running + + non_maching_time = start_time.replace(hour=3) + with patch("homeassistant.util.dt.utcnow", return_value=non_maching_time): + async_fire_time_changed(hass, non_maching_time) + + with timeout(0.1): + await hass.async_block_till_done() + except asyncio.TimeoutError: + timed_out = True + await script_obj.async_stop() + + assert timed_out + + @pytest.mark.parametrize("mode", ["no_timeout", "timeout_finish", "timeout_not_finish"]) @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_wait_variables_out(hass, mode, action_type): From 0e4c560f38b65d2b68b614dbc5050e3d7a802dc1 Mon Sep 17 00:00:00 2001 From: Sergio Oller Date: Fri, 8 Jan 2021 17:28:22 +0100 Subject: [PATCH 03/28] Disambiguate Supervisor HTTPUnauthorized on user/password validation (#44940) * Disambiguate HTTPUnauthorized on user/password validation The HA core API usually returns 401 when the request does not have proper authentication tokens or they have expired. However the user/password validation endpoint may also return 401 when the given user/password is invalid. The supervisor is currently unable to distinguish both scenarios, and it needs to. See https://github.com/home-assistant/supervisor/issues/2408 * Return 404 if user& password are not found/valid * Fix test for invalid user/password --- homeassistant/components/hassio/auth.py | 2 +- tests/components/hassio/test_auth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 23b91ac40bc..a1c032fe0fe 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -82,7 +82,7 @@ class HassIOAuth(HassIOBaseAuth): data[ATTR_USERNAME], data[ATTR_PASSWORD] ) except auth_ha.InvalidAuth: - raise HTTPUnauthorized() from None + raise HTTPNotFound() from None return web.Response(status=HTTP_OK) diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index c5ac9df74b7..3d6a339082c 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -66,7 +66,7 @@ async def test_login_error(hass, hassio_client_supervisor): ) # Check we got right response - assert resp.status == 401 + assert resp.status == 404 mock_login.assert_called_with("test", "123456") From 1b0e0996af9fd16e5050757a0c59ad9ed674dde7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 9 Jan 2021 01:10:47 +0100 Subject: [PATCH 04/28] Fix parameters when toggling light (#44950) --- homeassistant/components/light/__init__.py | 2 +- tests/components/light/test_init.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index fdef5e61a76..f406366dc86 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -232,7 +232,7 @@ async def async_setup(hass, config): async def async_handle_toggle_service(light, call): """Handle toggling a light.""" if light.is_on: - off_params = filter_turn_off_params(call.data) + off_params = filter_turn_off_params(call.data["params"]) await light.async_turn_off(**off_params) else: await async_handle_light_on_service(light, call) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index afc125e6423..72674a984fd 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -336,6 +336,21 @@ async def test_services(hass, mock_light_profiles): light.ATTR_TRANSITION: prof_t, } + await hass.services.async_call( + light.DOMAIN, + SERVICE_TOGGLE, + { + ATTR_ENTITY_ID: ent3.entity_id, + light.ATTR_TRANSITION: 4, + }, + blocking=True, + ) + + _, data = ent3.last_call("turn_off") + assert data == { + light.ATTR_TRANSITION: 4, + } + # Test bad data await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True From 21121b6e9b8bbed485a256c7cf8154f2f00266c0 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Sat, 9 Jan 2021 07:29:48 -0700 Subject: [PATCH 05/28] Bump pymyq to 2.0.13 (#44961) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 5f863ad7f34..653a2229296 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.12"], + "requirements": ["pymyq==2.0.13"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 8e3b891c2e7..47fd57aeef4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1536,7 +1536,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.12 +pymyq==2.0.13 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 466c072f4a9..0be9f1a1d96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -779,7 +779,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.12 +pymyq==2.0.13 # homeassistant.components.nut pynut2==2.1.2 From 9524766b07c7ab8a328c43eac18df85be6d30c71 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 9 Jan 2021 14:31:14 +0000 Subject: [PATCH 06/28] Bumped version to 2021.1.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4baa7e7bc3a..13ed75196d7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 1 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 3771a800d35ee9838cfe6be817426f99ab209907 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 13 Jan 2021 15:42:28 +0000 Subject: [PATCH 07/28] Update the Utility Meter sensor status on HA start (#44765) * fix status on HA start * better coverage and fix * fix test * address review --- .../components/utility_meter/sensor.py | 21 +++-- tests/components/utility_meter/test_sensor.py | 89 ++++++++++++++++++- 2 files changed, 101 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 6b25ec7d123..e8d551ba280 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -169,7 +169,11 @@ class UtilityMeterSensor(RestoreEntity): new_state = event.data.get("new_state") if new_state is None: return - if self._tariff == new_state.state: + + self._change_status(new_state.state) + + def _change_status(self, tariff): + if self._tariff == tariff: self._collecting = async_track_state_change_event( self.hass, [self._sensor_source_id], self.async_reading ) @@ -271,25 +275,26 @@ class UtilityMeterSensor(RestoreEntity): self._last_reset = dt_util.parse_datetime( state.attributes.get(ATTR_LAST_RESET) ) - self.async_write_ha_state() - if state.attributes.get(ATTR_STATUS) == PAUSED: - # Fake cancellation function to init the meter paused + if state.attributes.get(ATTR_STATUS) == COLLECTING: + # Fake cancellation function to init the meter in similar state self._collecting = lambda: None @callback def async_source_tracking(event): """Wait for source to be ready, then start meter.""" if self._tariff_entity is not None: - _LOGGER.debug("Track %s", self._tariff_entity) + _LOGGER.debug( + "<%s> tracks utility meter %s", self.name, self._tariff_entity + ) async_track_state_change_event( self.hass, [self._tariff_entity], self.async_tariff_change ) tariff_entity_state = self.hass.states.get(self._tariff_entity) - if self._tariff != tariff_entity_state.state: - return + self._change_status(tariff_entity_state.state) + return - _LOGGER.debug("tracking source: %s", self._sensor_source_id) + _LOGGER.debug("<%s> collecting from %s", self.name, self._sensor_source_id) self._collecting = async_track_state_change_event( self.hass, [self._sensor_source_id], self.async_reading ) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 2856a63b4f5..17459912175 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -10,17 +10,24 @@ from homeassistant.components.utility_meter.const import ( SERVICE_CALIBRATE_METER, SERVICE_SELECT_TARIFF, ) +from homeassistant.components.utility_meter.sensor import ( + ATTR_LAST_RESET, + ATTR_STATUS, + COLLECTING, + PAUSED, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.async_mock import patch -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache @contextmanager @@ -55,6 +62,21 @@ async def test_state(hass): ) await hass.async_block_till_done() + state = hass.states.get("sensor.energy_bill_onpeak") + assert state is not None + assert state.state == "0" + assert state.attributes.get("status") == COLLECTING + + state = hass.states.get("sensor.energy_bill_midpeak") + assert state is not None + assert state.state == "0" + assert state.attributes.get("status") == PAUSED + + state = hass.states.get("sensor.energy_bill_offpeak") + assert state is not None + assert state.state == "0" + assert state.attributes.get("status") == PAUSED + now = dt_util.utcnow() + timedelta(seconds=10) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set( @@ -68,14 +90,17 @@ async def test_state(hass): state = hass.states.get("sensor.energy_bill_onpeak") assert state is not None assert state.state == "1" + assert state.attributes.get("status") == COLLECTING state = hass.states.get("sensor.energy_bill_midpeak") assert state is not None assert state.state == "0" + assert state.attributes.get("status") == PAUSED state = hass.states.get("sensor.energy_bill_offpeak") assert state is not None assert state.state == "0" + assert state.attributes.get("status") == PAUSED await hass.services.async_call( DOMAIN, @@ -99,14 +124,17 @@ async def test_state(hass): state = hass.states.get("sensor.energy_bill_onpeak") assert state is not None assert state.state == "1" + assert state.attributes.get("status") == PAUSED state = hass.states.get("sensor.energy_bill_midpeak") assert state is not None assert state.state == "0" + assert state.attributes.get("status") == PAUSED state = hass.states.get("sensor.energy_bill_offpeak") assert state is not None assert state.state == "3" + assert state.attributes.get("status") == COLLECTING await hass.services.async_call( DOMAIN, @@ -131,6 +159,65 @@ async def test_state(hass): assert state.state == "0.123" +async def test_restore_state(hass): + """Test utility sensor restore state.""" + config = { + "utility_meter": { + "energy_bill": { + "source": "sensor.energy", + "tariffs": ["onpeak", "midpeak", "offpeak"], + } + } + } + mock_restore_cache( + hass, + [ + State( + "sensor.energy_bill_onpeak", + "3", + attributes={ + ATTR_STATUS: PAUSED, + ATTR_LAST_RESET: "2020-12-21T00:00:00.013073+00:00", + }, + ), + State( + "sensor.energy_bill_offpeak", + "6", + attributes={ + ATTR_STATUS: COLLECTING, + ATTR_LAST_RESET: "2020-12-21T00:00:00.013073+00:00", + }, + ), + ], + ) + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + # restore from cache + state = hass.states.get("sensor.energy_bill_onpeak") + assert state.state == "3" + assert state.attributes.get("status") == PAUSED + + state = hass.states.get("sensor.energy_bill_offpeak") + assert state.state == "6" + assert state.attributes.get("status") == COLLECTING + + # utility_meter is loaded, now set sensors according to utility_meter: + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + state = hass.states.get("utility_meter.energy_bill") + assert state.state == "onpeak" + + state = hass.states.get("sensor.energy_bill_onpeak") + assert state.attributes.get("status") == COLLECTING + + state = hass.states.get("sensor.energy_bill_offpeak") + assert state.attributes.get("status") == PAUSED + + async def test_net_consumption(hass): """Test utility sensor state.""" config = { From 2294f11070e0fdeb8c1c1b75a44046b3db453508 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 13 Jan 2021 15:25:28 +0100 Subject: [PATCH 08/28] Bump bimmer_connected to 0.7.14 (#45086) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 5bce904e1cd..c1d90f713f4 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.13"], + "requirements": ["bimmer_connected==0.7.14"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 47fd57aeef4..053c8968d1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ beautifulsoup4==4.9.1 bellows==0.21.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.13 +bimmer_connected==0.7.14 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0be9f1a1d96..ad5201332c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ base36==0.1.1 bellows==0.21.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.13 +bimmer_connected==0.7.14 # homeassistant.components.blebox blebox_uniapi==1.3.2 From f141f18db760ccbe37f7f1181ba3b8f8b74def42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Jan 2021 19:08:37 +0000 Subject: [PATCH 09/28] Bumped version to 2021.1.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 13ed75196d7..582a5260fb5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 1 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From d0216307f373763f14e51711c8d18f4f66f56a32 Mon Sep 17 00:00:00 2001 From: Santobert Date: Wed, 13 Jan 2021 10:23:16 +0100 Subject: [PATCH 10/28] Fix neato battery sensor not ready (#44946) * Fix neato battery sensor not ready * Edit available attribute * Remove unnecessary condition --- homeassistant/components/neato/sensor.py | 2 +- homeassistant/components/neato/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index b083ec1d7df..50af42e7007 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -37,7 +37,7 @@ class NeatoSensor(Entity): def __init__(self, neato, robot): """Initialize Neato sensor.""" self.robot = robot - self._available = neato is not None + self._available = False self._robot_name = f"{self.robot.name} {BATTERY}" self._robot_serial = self.robot.serial self._state = None diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 204adb108a8..a3cc51b82c6 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -40,7 +40,7 @@ class NeatoConnectedSwitch(ToggleEntity): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self._available = neato is not None + self._available = False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None From e1989399f046033ac64f759e9e092a0ba4c9136d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 12 Jan 2021 10:33:14 +0100 Subject: [PATCH 11/28] Fallback to tag for any AfterShip tracking that have no checkpoints (#45053) --- homeassistant/components/aftership/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index 07267c1185d..2d9021f8009 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -181,8 +181,8 @@ class AfterShipSensor(Entity): track["tracking_number"] if track["title"] is None else track["title"] ) last_checkpoint = ( - "Shipment pending" - if track["tag"] == "Pending" + f"Shipment {track['tag'].lower()}" + if not track["checkpoints"] else track["checkpoints"][-1] ) status_counts[status] = status_counts.get(status, 0) + 1 From 7ac208ead6a5af203ff6ca5b7ccbeaa4c1bc4b07 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 13 Jan 2021 23:15:23 -0700 Subject: [PATCH 12/28] Bump MyQ to 2.0.14 (#45067) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 653a2229296..aba2f24b5bd 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.13"], + "requirements": ["pymyq==2.0.14"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 053c8968d1f..eaca17a952e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1536,7 +1536,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.13 +pymyq==2.0.14 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad5201332c2..a8d21dbaa42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -779,7 +779,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.13 +pymyq==2.0.14 # homeassistant.components.nut pynut2==2.1.2 From 0fdda9d0f64dc54731dcf0d610889fad99929b74 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 14 Jan 2021 08:47:45 +0100 Subject: [PATCH 13/28] Fix OpenWeatherMap forecast timestamp (#45124) --- homeassistant/components/openweathermap/sensor.py | 10 +++++++++- .../openweathermap/weather_update_coordinator.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 39c50c3b941..b1ba4ab7625 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -1,7 +1,10 @@ """Support for the OpenWeatherMap (OWM) service.""" +import datetime + from .abstract_owm_sensor import AbstractOpenWeatherMapSensor from .const import ( ATTR_API_FORECAST, + DEVICE_CLASS_TIMESTAMP, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, @@ -95,5 +98,10 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): """Return the state of the device.""" forecasts = self._weather_coordinator.data.get(ATTR_API_FORECAST) if forecasts is not None and len(forecasts) > 0: - return forecasts[0].get(self._sensor_type, None) + value = forecasts[0].get(self._sensor_type, None) + if self._device_class is DEVICE_CLASS_TIMESTAMP: + value = datetime.datetime.fromtimestamp( + value, datetime.timezone.utc + ).isoformat() + return value return None diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index b4ddb40c046..605e6f9edc1 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -138,7 +138,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _convert_forecast(self, entry): forecast = { - ATTR_FORECAST_TIME: entry.reference_time("unix") * 1000, + ATTR_FORECAST_TIME: entry.reference_time("unix"), ATTR_FORECAST_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), From 0bd2c13e261c7b3d96ba451c50f81dd3e659c5c9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jan 2021 20:02:01 +0100 Subject: [PATCH 14/28] Add filtering --- homeassistant/components/http/__init__.py | 6 +- .../components/http/security_filter.py | 51 ++++++++++++++++ tests/components/http/test_security_filter.py | 58 +++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/http/security_filter.py create mode 100644 tests/components/http/test_security_filter.py diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 9e47dd29a23..7f70d49f686 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -30,6 +30,7 @@ from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER # noqa: F401 from .cors import setup_cors from .forwarded import async_setup_forwarded from .request_context import setup_request_context +from .security_filter import setup_security_filter from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa: F401 from .web_runner import HomeAssistantTCPSite @@ -296,7 +297,10 @@ class HomeAssistantHTTP: ) app[KEY_HASS] = hass - # Order matters, forwarded middleware needs to go first. + # Order matters, security filters middle ware needs to go first, + # forwarded middleware needs to go second. + setup_security_filter(app) + # Only register middleware if `use_x_forwarded_for` is enabled # and trusted proxies are provided if use_x_forwarded_for and trusted_proxies: diff --git a/homeassistant/components/http/security_filter.py b/homeassistant/components/http/security_filter.py new file mode 100644 index 00000000000..32ebcacfff4 --- /dev/null +++ b/homeassistant/components/http/security_filter.py @@ -0,0 +1,51 @@ +"""Middleware to add some basic security filtering to requests.""" +import logging +import re + +from aiohttp.web import HTTPBadRequest, middleware + +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +# mypy: allow-untyped-defs + +# fmt: off +FILTERS = re.compile( + r"(?:" + + # Common exploits + r"proc/self/environ" + r"|(<|%3C).*script.*(>|%3E)" + + # File Injections + r"|(\.\.//?)+" # ../../anywhere + r"|[a-zA-Z0-9_]=/([a-z0-9_.]//?)+" # .html?v=/.//test + + # SQL Injections + r"|union.*select.*\(" + r"|union.*all.*select.*" + r"|concat.*\(" + + r")", + flags=re.IGNORECASE, +) +# fmt: on + + +@callback +def setup_security_filter(app): + """Create security filter middleware for the app.""" + + @middleware + async def security_filter_middleware(request, handler): + """Process request and block commonly known exploit attempts.""" + if FILTERS.search(request.raw_path): + _LOGGER.warning( + "Filtered a potential harmful request to: %s", request.raw_path + ) + raise HTTPBadRequest + + return await handler(request) + + app.middlewares.append(security_filter_middleware) diff --git a/tests/components/http/test_security_filter.py b/tests/components/http/test_security_filter.py new file mode 100644 index 00000000000..8190c514603 --- /dev/null +++ b/tests/components/http/test_security_filter.py @@ -0,0 +1,58 @@ +"""Test security filter middleware.""" +from aiohttp import web +import pytest + +from homeassistant.components.http.security_filter import setup_security_filter + + +async def mock_handler(request): + """Return OK.""" + return web.Response(text="OK") + + +@pytest.mark.parametrize( + "request_path,request_params", + [ + ("/", {}), + ("/lovelace/dashboard", {}), + ("/frontend_latest/chunk.4c9e2d8dc10f77b885b0.js", {}), + ("/static/translations/en-f96a262a5a6eede29234dc45dc63abf2.json", {}), + ("/", {"test": "123"}), + ], +) +async def test_ok_requests(request_path, request_params, aiohttp_client): + """Test request paths that should not be filtered.""" + app = web.Application() + app.router.add_get("/{all:.*}", mock_handler) + + setup_security_filter(app) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get(request_path, params=request_params) + + assert resp.status == 200 + assert await resp.text() == "OK" + + +@pytest.mark.parametrize( + "request_path,request_params", + [ + ("/proc/self/environ", {}), + ("/", {"test": "/test/../../api"}), + ("/", {"test": "test/../../api"}), + ("/", {"sql": ";UNION SELECT (a, b"}), + ("/", {"sql": "concat(..."}), + ("/", {"xss": "