From e0ca5bafda8b1288129a7e5675ece48d9490d14e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Jun 2022 10:04:46 -1000 Subject: [PATCH 01/12] Fix statistics_during_period being incorrectly cached (#72947) --- .../components/recorder/statistics.py | 7 +- tests/components/history/test_init.py | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4bed39fee4a..39fcb954ee9 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -984,7 +984,6 @@ def _reduce_statistics_per_month( def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, - statistic_ids: list[str] | None, metadata_ids: list[int] | None, table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: @@ -1002,7 +1001,7 @@ def _statistics_during_period_stmt( if end_time is not None: stmt += lambda q: q.filter(table.start < end_time) - if statistic_ids is not None: + if metadata_ids: stmt += lambda q: q.filter(table.metadata_id.in_(metadata_ids)) stmt += lambda q: q.order_by(table.metadata_id, table.start) @@ -1038,9 +1037,7 @@ def statistics_during_period( else: table = Statistics - stmt = _statistics_during_period_stmt( - start_time, end_time, statistic_ids, metadata_ids, table - ) + stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids, table) stats = execute_stmt_lambda_element(session, stmt) if not stats: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 9dc7af59a38..8c0a80719a8 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,6 +5,7 @@ from http import HTTPStatus import json from unittest.mock import patch, sentinel +from freezegun import freeze_time import pytest from pytest import approx @@ -928,6 +929,141 @@ async def test_statistics_during_period( } +@pytest.mark.parametrize( + "units, attributes, state, value", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), + ], +) +async def test_statistics_during_period_in_the_past( + hass, hass_ws_client, recorder_mock, units, attributes, state, value +): + """Test statistics_during_period in the past.""" + hass.config.set_time_zone("UTC") + now = dt_util.utcnow().replace() + + hass.config.units = units + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + past = now - timedelta(days=3) + + with freeze_time(past): + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + sensor_state = hass.states.get("sensor.test") + assert sensor_state.last_updated == past + + stats_top_of_hour = past.replace(minute=0, second=0, microsecond=0) + stats_start = past.replace(minute=55) + do_adhoc_statistics(hass, start=stats_start) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + past = now - timedelta(days=3) + await client.send_json( + { + "id": 3, + "type": "history/statistics_during_period", + "start_time": past.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": stats_start.isoformat(), + "end": (stats_start + timedelta(minutes=5)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + start_of_day = stats_top_of_hour.replace(hour=0, minute=0) + await client.send_json( + { + "id": 4, + "type": "history/statistics_during_period", + "start_time": stats_top_of_hour.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": start_of_day.isoformat(), + "end": (start_of_day + timedelta(days=1)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + await client.send_json( + { + "id": 5, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + async def test_statistics_during_period_bad_start_time( hass, hass_ws_client, recorder_mock ): From 73536c07d7a13e5f7c5a2c5d6c07d8b4ec2c5d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Beamonte?= Date: Fri, 3 Jun 2022 09:27:10 -0400 Subject: [PATCH 02/12] Allow `log` template function to return specified `default` on math domain error (#72960) Fix regression for logarithm template --- homeassistant/helpers/template.py | 14 +++++++------- tests/helpers/test_template.py | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1a8febf5ac2..053beab307e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1369,19 +1369,19 @@ def multiply(value, amount, default=_SENTINEL): def logarithm(value, base=math.e, default=_SENTINEL): """Filter and function to get logarithm of the value with a specific base.""" - try: - value_float = float(value) - except (ValueError, TypeError): - if default is _SENTINEL: - raise_no_default("log", value) - return default try: base_float = float(base) except (ValueError, TypeError): if default is _SENTINEL: raise_no_default("log", base) return default - return math.log(value_float, base_float) + try: + value_float = float(value) + return math.log(value_float, base_float) + except (ValueError, TypeError): + if default is _SENTINEL: + raise_no_default("log", value) + return default def sine(value, default=_SENTINEL): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index ddda17c20ac..a1fd3e73f59 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -447,6 +447,8 @@ def test_logarithm(hass): assert render(hass, "{{ 'no_number' | log(10, default=1) }}") == 1 assert render(hass, "{{ log('no_number', 10, 1) }}") == 1 assert render(hass, "{{ log('no_number', 10, default=1) }}") == 1 + assert render(hass, "{{ log(0, 10, 1) }}") == 1 + assert render(hass, "{{ log(0, 10, default=1) }}") == 1 def test_sine(hass): From 9f8fe7fca6dfee9fca4533c93ec81899190199b6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 3 Jun 2022 13:57:59 +0200 Subject: [PATCH 03/12] Bump pynetgear to 0.10.4 (#72965) bump pynetgear to 0.10.4 --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index ae0824c82a5..f65f5aa6686 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.0"], + "requirements": ["pynetgear==0.10.4"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 791875978ef..454423a6dca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1673,7 +1673,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.0 +pynetgear==0.10.4 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4fa7c37963d..b6426228b42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1131,7 +1131,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.0 +pynetgear==0.10.4 # homeassistant.components.nina pynina==0.1.8 From 65cb82765baaace47b8933d4413b542fff9062f8 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 3 Jun 2022 21:59:10 +0200 Subject: [PATCH 04/12] Bump bimmer_connected to 0.9.4 (#72973) 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 75ac3e982e8..cd6daa83705 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.9.3"], + "requirements": ["bimmer_connected==0.9.4"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 454423a6dca..811d63d8e2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -394,7 +394,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.3 +bimmer_connected==0.9.4 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6426228b42..67fc6dfe509 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.3 +bimmer_connected==0.9.4 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 5c512ad5cbbaf5315ebbfc5601dcd0c96add4630 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 3 Jun 2022 22:05:37 +0200 Subject: [PATCH 05/12] fjaraskupan: Don't filter anything in backend (#72988) --- homeassistant/components/fjaraskupan/__init__.py | 4 ++-- homeassistant/components/fjaraskupan/config_flow.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index ec4528bc079..4c4f19403a6 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -9,7 +9,7 @@ import logging from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from fjaraskupan import DEVICE_NAME, Device, State, device_filter +from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -90,7 +90,7 @@ class EntryState: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"Pattern": DEVICE_NAME, "DuplicateData": True}) + scanner = BleakScanner(filters={"DuplicateData": True}) state = EntryState(scanner, {}) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index 3af34c0eef6..ffac366500b 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -7,7 +7,7 @@ import async_timeout from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from fjaraskupan import DEVICE_NAME, device_filter +from fjaraskupan import device_filter from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow @@ -28,7 +28,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: async with BleakScanner( detection_callback=detection, - filters={"Pattern": DEVICE_NAME, "DuplicateData": True}, + filters={"DuplicateData": True}, ): try: async with async_timeout.timeout(CONST_WAIT_TIME): From 6a3b74adf64a848ee20fc1ad2d0c148676792a71 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 3 Jun 2022 11:53:23 -0500 Subject: [PATCH 06/12] Check ISY994 climate for unknown humidity value on Z-Wave Thermostat (#72990) Check ISY994 climate for unknown humidity on Z-Wave Thermostat Update to #72670 to compare the property value and not the parent object. Should actually fix #72628 --- homeassistant/components/isy994/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index d68395f14da..9f4c52258a7 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -117,7 +117,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Return the current humidity.""" if not (humidity := self._node.aux_properties.get(PROP_HUMIDITY)): return None - if humidity == ISY_VALUE_UNKNOWN: + if humidity.value == ISY_VALUE_UNKNOWN: return None return int(humidity.value) From 085eee88c97426fd989260f5845f380c78899c81 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 3 Jun 2022 16:33:12 -0700 Subject: [PATCH 07/12] Fix google calendar bug where expired tokens are not refreshed (#72994) --- homeassistant/components/google/api.py | 7 +++++-- tests/components/google/conftest.py | 6 ++++-- tests/components/google/test_config_flow.py | 14 ++++++++------ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 4bb9de5d581..a4cda1ff41a 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import datetime import logging -import time from typing import Any, cast import aiohttp @@ -50,12 +49,16 @@ class DeviceAuth(AuthImplementation): async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve a Google API Credentials object to Home Assistant token.""" creds: Credentials = external_data[DEVICE_AUTH_CREDS] + delta = creds.token_expiry.replace(tzinfo=datetime.timezone.utc) - dt.utcnow() + _LOGGER.debug( + "Token expires at %s (in %s)", creds.token_expiry, delta.total_seconds() + ) return { "access_token": creds.access_token, "refresh_token": creds.refresh_token, "scope": " ".join(creds.scopes), "token_type": "Bearer", - "expires_in": creds.token_expiry.timestamp() - time.time(), + "expires_in": delta.total_seconds(), } diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index b5566450913..68176493445 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -16,7 +16,6 @@ from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -136,7 +135,10 @@ def token_scopes() -> list[str]: @pytest.fixture def token_expiry() -> datetime.datetime: """Expiration time for credentials used in the test.""" - return utcnow() + datetime.timedelta(days=7) + # OAuth library returns an offset-naive timestamp + return datetime.datetime.fromtimestamp( + datetime.datetime.utcnow().timestamp() + ) + datetime.timedelta(hours=1) @pytest.fixture diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 8ac017fcba4..a346b02e6c2 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import Mock, patch from aiohttp.client_exceptions import ClientError +from freezegun.api import FrozenDateTimeFactory from oauth2client.client import ( FlowExchangeError, OAuth2Credentials, @@ -94,11 +95,13 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() +@pytest.mark.freeze_time("2022-06-03 15:19:59-00:00") async def test_full_flow_yaml_creds( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, component_setup: ComponentSetup, + freezer: FrozenDateTimeFactory, ) -> None: """Test successful creds setup.""" assert await component_setup() @@ -115,8 +118,8 @@ async def test_full_flow_yaml_creds( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + freezer.tick(CODE_CHECK_ALARM_TIMEDELTA) + await fire_alarm(hass, datetime.datetime.utcnow()) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] @@ -127,12 +130,11 @@ async def test_full_flow_yaml_creds( assert "data" in result data = result["data"] assert "token" in data - assert 0 < data["token"]["expires_in"] < 8 * 86400 assert ( - datetime.datetime.now().timestamp() - <= data["token"]["expires_at"] - < (datetime.datetime.now() + datetime.timedelta(days=8)).timestamp() + data["token"]["expires_in"] + == 60 * 60 - CODE_CHECK_ALARM_TIMEDELTA.total_seconds() ) + assert data["token"]["expires_at"] == 1654273199.0 data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { From f3136c811c41e94f1614331d2208f562292dc90f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 3 Jun 2022 17:03:21 -0500 Subject: [PATCH 08/12] Provide Sonos media position if duration not available (#73001) --- homeassistant/components/sonos/media.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index e3d8f043d4b..9608356ba64 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -205,13 +205,15 @@ class SonosMedia: self, position_info: dict[str, int], force_update: bool = False ) -> None: """Update state when playing music tracks.""" - if (duration := position_info.get(DURATION_SECONDS)) == 0: + duration = position_info.get(DURATION_SECONDS) + current_position = position_info.get(POSITION_SECONDS) + + if not (duration or current_position): self.clear_position() return should_update = force_update self.duration = duration - current_position = position_info.get(POSITION_SECONDS) # player started reporting position? if current_position is not None and self.position is None: From 10fb3035d6a59fcdb1d6076223779b183f9048b8 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 4 Jun 2022 07:52:39 +0200 Subject: [PATCH 09/12] Bump pypck to 0.7.15 (#73009) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 412ef74e3b8..eea72a0e508 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,7 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.14"], + "requirements": ["pypck==0.7.15"], "codeowners": ["@alengwenus"], "iot_class": "local_push", "loggers": ["pypck"] diff --git a/requirements_all.txt b/requirements_all.txt index 811d63d8e2b..4c848eef637 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1735,7 +1735,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.14 +pypck==0.7.15 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67fc6dfe509..3efb7dd9878 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1178,7 +1178,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.14 +pypck==0.7.15 # homeassistant.components.plaato pyplaato==0.0.18 From 373634cc50d67fb47b444deaab98939f55d0ab59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 09:54:10 -1000 Subject: [PATCH 10/12] Fix missing historical context data in logbook for MySQL and PostgreSQL (#73011) --- homeassistant/components/recorder/filters.py | 29 ++++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 835496c2d6e..90851e9f251 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Iterable import json from typing import Any -from sqlalchemy import JSON, Column, Text, cast, not_, or_ +from sqlalchemy import Column, Text, cast, not_, or_ from sqlalchemy.sql.elements import ClauseList from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE @@ -16,6 +16,7 @@ from .models import ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT, States DOMAIN = "history" HISTORY_FILTERS = "history_filters" +JSON_NULL = json.dumps(None) GLOB_TO_SQL_CHARS = { ord("*"): "%", @@ -196,7 +197,17 @@ class Filters: """Generate the entity filter query.""" _encoder = json.dumps return or_( - (ENTITY_ID_IN_EVENT == JSON.NULL) & (OLD_ENTITY_ID_IN_EVENT == JSON.NULL), + # sqlalchemy's SQLite json implementation always + # wraps everything with JSON_QUOTE so it resolves to 'null' + # when its empty + # + # For MySQL and PostgreSQL it will resolve to a literal + # NULL when its empty + # + ((ENTITY_ID_IN_EVENT == JSON_NULL) | ENTITY_ID_IN_EVENT.is_(None)) + & ( + (OLD_ENTITY_ID_IN_EVENT == JSON_NULL) | OLD_ENTITY_ID_IN_EVENT.is_(None) + ), self._generate_filter_for_columns( (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), _encoder ).self_group(), @@ -208,8 +219,11 @@ def _globs_to_like( ) -> ClauseList: """Translate glob to sql.""" matchers = [ - cast(column, Text()).like( - encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ( + column.is_not(None) + & cast(column, Text()).like( + encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ) ) for glob_str in glob_strs for column in columns @@ -221,7 +235,10 @@ def _entity_matcher( entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) + ( + column.is_not(None) + & cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) + ) for column in columns ] return or_(*matchers) if matchers else or_(False) @@ -231,7 +248,7 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - cast(column, Text()).like(encoder(f"{domain}.%")) + (column.is_not(None) & cast(column, Text()).like(encoder(f"{domain}.%"))) for domain in domains for column in columns ] From b401f165830b31eb7c17516ab8407ffdfad7fc41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 09:47:30 -1000 Subject: [PATCH 11/12] Fix history stats not comparing all times in UTC (#73040) --- homeassistant/components/history_stats/data.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 5466498fc32..3b17c715c97 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -67,9 +67,10 @@ class HistoryStats: current_period_end_timestamp = floored_timestamp(current_period_end) previous_period_start_timestamp = floored_timestamp(previous_period_start) previous_period_end_timestamp = floored_timestamp(previous_period_end) - now_timestamp = floored_timestamp(datetime.datetime.now()) + utc_now = dt_util.utcnow() + now_timestamp = floored_timestamp(utc_now) - if now_timestamp < current_period_start_timestamp: + if current_period_start > utc_now: # History cannot tell the future self._history_current_period = [] self._previous_run_before_start = True From d9a41d10ffbc2a9f7d0aca8673d81e143993b438 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Jun 2022 13:03:51 -0700 Subject: [PATCH 12/12] Bumped version to 2022.6.2 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 08ab335a33b..23ada7591ee 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index aeafbd53565..08bd61e7382 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.1 +version = 2022.6.2 url = https://www.home-assistant.io/ [options]