Merge pull request #73041 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-06-04 14:13:48 -07:00 committed by GitHub
commit c0482bdbfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 210 additions and 48 deletions

View File

@ -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",

View File

@ -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, {})

View File

@ -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):

View File

@ -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(),
}

View File

@ -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

View File

@ -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)

View File

@ -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"]

View File

@ -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,

View File

@ -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
]

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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):

View File

@ -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
@ -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
@ -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

View File

@ -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
@ -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
@ -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

View File

@ -1,5 +1,5 @@
[metadata]
version = 2022.6.1
version = 2022.6.2
url = https://www.home-assistant.io/
[options]

View File

@ -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

View File

@ -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 == {

View File

@ -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
):

View File

@ -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):