Merge pull request #62882 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2021-12-27 23:25:51 +01:00 committed by GitHub
commit dfe193b277
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 104 additions and 51 deletions

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20211220.0"
"home-assistant-frontend==20211227.0"
],
"dependencies": [
"api",

View File

@ -641,6 +641,8 @@ class EnergyStorageTrait(_Trait):
def query_attributes(self):
"""Return EnergyStorage query attributes."""
battery_level = self.state.attributes.get(ATTR_BATTERY_LEVEL)
if battery_level is None:
return {}
if battery_level == 100:
descriptive_capacity_remaining = "FULL"
elif 75 <= battery_level < 100:

View File

@ -145,6 +145,7 @@ class HueBaseEntity(Entity):
if self.device.product_data.certified:
# certified products report their state correctly
self._ignore_availability = False
return
# some (3th party) Hue lights report their connection status incorrectly
# causing the zigbee availability to report as disconnected while in fact
# it can be controlled. Although this is in fact something the device manufacturer

View File

@ -3,11 +3,13 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Literal
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.const import CURRENCY_EURO, DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
DOMAIN = "picnic"
@ -42,7 +44,7 @@ class PicnicRequiredKeysMixin:
"""Mixin for required keys."""
data_type: Literal["cart_data", "slot_data", "last_order_data"]
value_fn: Callable[[Any], StateType]
value_fn: Callable[[Any], StateType | datetime]
@dataclass
@ -73,7 +75,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:calendar-start",
entity_registry_enabled_default=True,
data_type="slot_data",
value_fn=lambda slot: slot.get("window_start"),
value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_start"))),
),
PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_END,
@ -81,7 +83,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:calendar-end",
entity_registry_enabled_default=True,
data_type="slot_data",
value_fn=lambda slot: slot.get("window_end"),
value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_end"))),
),
PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME,
@ -89,7 +91,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:clock-alert-outline",
entity_registry_enabled_default=True,
data_type="slot_data",
value_fn=lambda slot: slot.get("cut_off_time"),
value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("cut_off_time"))),
),
PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE,
@ -108,14 +110,18 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
device_class=DEVICE_CLASS_TIMESTAMP,
icon="mdi:calendar-start",
data_type="last_order_data",
value_fn=lambda last_order: last_order.get("slot", {}).get("window_start"),
value_fn=lambda last_order: dt_util.parse_datetime(
str(last_order.get("slot", {}).get("window_start"))
),
),
PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_SLOT_END,
device_class=DEVICE_CLASS_TIMESTAMP,
icon="mdi:calendar-end",
data_type="last_order_data",
value_fn=lambda last_order: last_order.get("slot", {}).get("window_end"),
value_fn=lambda last_order: dt_util.parse_datetime(
str(last_order.get("slot", {}).get("window_end"))
),
),
PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_STATUS,
@ -129,7 +135,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:clock-start",
entity_registry_enabled_default=True,
data_type="last_order_data",
value_fn=lambda last_order: last_order.get("eta", {}).get("start"),
value_fn=lambda last_order: dt_util.parse_datetime(
str(last_order.get("eta", {}).get("start"))
),
),
PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_ETA_END,
@ -137,7 +145,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:clock-end",
entity_registry_enabled_default=True,
data_type="last_order_data",
value_fn=lambda last_order: last_order.get("eta", {}).get("end"),
value_fn=lambda last_order: dt_util.parse_datetime(
str(last_order.get("eta", {}).get("end"))
),
),
PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_DELIVERY_TIME,
@ -145,7 +155,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:timeline-clock",
entity_registry_enabled_default=True,
data_type="last_order_data",
value_fn=lambda last_order: last_order.get("delivery_time", {}).get("start"),
value_fn=lambda last_order: dt_util.parse_datetime(
str(last_order.get("delivery_time", {}).get("start"))
),
),
PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_TOTAL_PRICE,

View File

@ -60,11 +60,11 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator):
"""Fetch the data from the Picnic API and return a flat dict with only needed sensor data."""
# Fetch from the API and pre-process the data
cart = self.picnic_api_client.get_cart()
last_order = self._get_last_order()
if not cart or not last_order:
if not cart:
raise UpdateFailed("API response doesn't contain expected data.")
last_order = self._get_last_order()
slot_data = self._get_slot_data(cart)
return {
@ -102,11 +102,12 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator):
"""Get data of the last order from the list of deliveries."""
# Get the deliveries
deliveries = self.picnic_api_client.get_deliveries(summary=True)
if not deliveries:
return {}
# Determine the last order
last_order = copy.deepcopy(deliveries[0])
# Determine the last order and return an empty dict if there is none
try:
last_order = copy.deepcopy(deliveries[0])
except KeyError:
return {}
# Get the position details if the order is not delivered yet
delivery_position = {}

View File

@ -1,6 +1,7 @@
"""Definition of Picnic sensors."""
from __future__ import annotations
from datetime import datetime
from typing import Any, cast
from homeassistant.components.sensor import SensorEntity
@ -62,8 +63,8 @@ class PicnicSensor(SensorEntity, CoordinatorEntity):
self._attr_unique_id = f"{config_entry.unique_id}.{description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
def native_value(self) -> StateType | datetime:
"""Return the value reported by the sensor."""
data_set = (
self.coordinator.data.get(self.entity_description.data_type, {})
if self.coordinator.data is not None
@ -73,8 +74,8 @@ class PicnicSensor(SensorEntity, CoordinatorEntity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.coordinator.last_update_success and self.state is not None
"""Return True if last update was successful."""
return self.coordinator.last_update_success
@property
def device_info(self) -> DeviceInfo:

View File

@ -3,7 +3,7 @@
"name": "iRobot Roomba and Braava",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roomba",
"requirements": ["roombapy==1.6.4"],
"requirements": ["roombapy==1.6.5"],
"codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"],
"dhcp": [
{

View File

@ -283,6 +283,7 @@ RPC_SENSORS: Final = {
device_class=sensor.DEVICE_CLASS_TEMPERATURE,
state_class=sensor.STATE_CLASS_MEASUREMENT,
default_enabled=False,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
"rssi": RpcAttributeDescription(
key="wifi",

View File

@ -3,7 +3,7 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.25.1"],
"requirements": ["soco==0.25.2"],
"dependencies": ["ssdp"],
"after_dependencies": ["plex", "zeroconf"],
"zeroconf": ["_sonos._tcp.local."],

View File

@ -482,7 +482,7 @@ class SonosSpeaker:
for bool_var in (
"dialog_level",
"night_mode",
"night_level",
"sub_enabled",
"surround_enabled",
):
@ -965,7 +965,7 @@ class SonosSpeaker:
self.volume = self.soco.volume
self.muted = self.soco.mute
self.night_mode = self.soco.night_mode
self.dialog_level = self.soco.dialog_mode
self.dialog_level = self.soco.dialog_level
self.bass = self.soco.bass
self.treble = self.soco.treble

View File

@ -36,7 +36,7 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones"
ATTR_CROSSFADE = "cross_fade"
ATTR_NIGHT_SOUND = "night_mode"
ATTR_SPEECH_ENHANCEMENT = "dialog_mode"
ATTR_SPEECH_ENHANCEMENT = "dialog_level"
ATTR_STATUS_LIGHT = "status_light"
ATTR_SUB_ENABLED = "sub_enabled"
ATTR_SURROUND_ENABLED = "surround_enabled"

View File

@ -2,7 +2,7 @@
"domain": "tuya",
"name": "Tuya",
"documentation": "https://www.home-assistant.io/integrations/tuya",
"requirements": ["tuya-iot-py-sdk==0.6.3"],
"requirements": ["tuya-iot-py-sdk==0.6.6"],
"dependencies": ["ffmpeg"],
"codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"],
"config_flow": true,

View File

@ -3,7 +3,7 @@
"name": "YouLess",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/youless",
"requirements": ["youless-api==0.15"],
"requirements": ["youless-api==0.16"],
"codeowners": ["@gjong"],
"iot_class": "local_polling"
}

View File

@ -2,7 +2,7 @@
"domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
"requirements": ["zeroconf==0.37.0"],
"requirements": ["zeroconf==0.38.1"],
"dependencies": ["network", "api"],
"codeowners": ["@bdraco"],
"quality_scale": "internal",

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "5"
PATCH_VERSION: Final = "6"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@ -16,7 +16,7 @@ ciso8601==2.2.0
cryptography==35.0.0
emoji==1.5.0
hass-nabucasa==0.50.0
home-assistant-frontend==20211220.0
home-assistant-frontend==20211227.0
httpx==0.21.0
ifaddr==0.1.7
jinja2==3.0.3
@ -33,7 +33,7 @@ sqlalchemy==1.4.27
voluptuous-serialize==2.5.0
voluptuous==0.12.2
yarl==1.6.3
zeroconf==0.37.0
zeroconf==0.38.1
pycryptodome>=3.6.6

View File

@ -820,7 +820,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211220.0
home-assistant-frontend==20211227.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -2074,7 +2074,7 @@ rocketchat-API==0.6.1
rokuecp==0.8.4
# homeassistant.components.roomba
roombapy==1.6.4
roombapy==1.6.5
# homeassistant.components.roon
roonapi==0.0.38
@ -2185,7 +2185,7 @@ smhi-pkg==1.0.15
snapcast==2.1.3
# homeassistant.components.sonos
soco==0.25.1
soco==0.25.2
# homeassistant.components.solaredge_local
solaredge-local==0.2.0
@ -2339,7 +2339,7 @@ tp-connected==0.0.4
transmissionrpc==0.11
# homeassistant.components.tuya
tuya-iot-py-sdk==0.6.3
tuya-iot-py-sdk==0.6.6
# homeassistant.components.twentemilieu
twentemilieu==0.5.0
@ -2475,7 +2475,7 @@ yeelight==0.7.8
yeelightsunflower==0.0.10
# homeassistant.components.youless
youless-api==0.15
youless-api==0.16
# homeassistant.components.media_extractor
youtube_dl==2021.06.06
@ -2484,7 +2484,7 @@ youtube_dl==2021.06.06
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.37.0
zeroconf==0.38.1
# homeassistant.components.zha
zha-quirks==0.0.65

View File

@ -515,7 +515,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211220.0
home-assistant-frontend==20211227.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -1236,7 +1236,7 @@ ring_doorbell==0.7.2
rokuecp==0.8.4
# homeassistant.components.roomba
roombapy==1.6.4
roombapy==1.6.5
# homeassistant.components.roon
roonapi==0.0.38
@ -1291,7 +1291,7 @@ smarthab==0.21
smhi-pkg==1.0.15
# homeassistant.components.sonos
soco==0.25.1
soco==0.25.2
# homeassistant.components.solaredge
solaredge==0.0.2
@ -1376,7 +1376,7 @@ total_connect_client==2021.12
transmissionrpc==0.11
# homeassistant.components.tuya
tuya-iot-py-sdk==0.6.3
tuya-iot-py-sdk==0.6.6
# homeassistant.components.twentemilieu
twentemilieu==0.5.0
@ -1470,10 +1470,10 @@ yalexs==1.1.13
yeelight==0.7.8
# homeassistant.components.youless
youless-api==0.15
youless-api==0.16
# homeassistant.components.zeroconf
zeroconf==0.37.0
zeroconf==0.38.1
# homeassistant.components.zha
zha-quirks==0.0.65

View File

@ -1,6 +1,7 @@
"""The tests for the Picnic sensor platform."""
import copy
from datetime import timedelta
from typing import Dict
import unittest
from unittest.mock import patch
@ -15,6 +16,7 @@ from homeassistant.const import (
CURRENCY_EURO,
DEVICE_CLASS_TIMESTAMP,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.util import dt
@ -103,6 +105,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
# Patch the api client
self.picnic_patcher = patch("homeassistant.components.picnic.PicnicAPI")
self.picnic_mock = self.picnic_patcher.start()
self.picnic_mock().session.auth_token = "3q29fpwhulzes"
# Add a config entry and setup the integration
config_data = {
@ -281,13 +284,11 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
await self._setup_platform()
# Assert sensors are unknown
self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNAVAILABLE)
self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNAVAILABLE)
self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNKNOWN)
self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNKNOWN)
self._assert_sensor("sensor.picnic_selected_slot_max_order_time", STATE_UNKNOWN)
self._assert_sensor(
"sensor.picnic_selected_slot_max_order_time", STATE_UNAVAILABLE
)
self._assert_sensor(
"sensor.picnic_selected_slot_min_order_value", STATE_UNAVAILABLE
"sensor.picnic_selected_slot_min_order_value", STATE_UNKNOWN
)
async def test_sensors_last_order_in_future(self):
@ -304,7 +305,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
await self._setup_platform()
# Assert delivery time is not available, but eta is
self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE)
self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN)
self._assert_sensor(
"sensor.picnic_last_order_eta_start", "2021-02-26T19:54:00+00:00"
)
@ -312,6 +313,25 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
"sensor.picnic_last_order_eta_end", "2021-02-26T20:14:00+00:00"
)
async def test_sensors_eta_date_malformed(self):
"""Test sensor states when last order eta dates are malformed."""
# Set-up platform with default mock responses
await self._setup_platform(use_default_responses=True)
# Set non-datetime strings as eta
eta_dates: Dict[str, str] = {
"start": "wrong-time",
"end": "other-malformed-datetime",
}
delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE)
delivery_response["eta2"] = eta_dates
self.picnic_mock().get_deliveries.return_value = [delivery_response]
await self._coordinator.async_refresh()
# Assert eta times are not available due to malformed date strings
self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNKNOWN)
self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNKNOWN)
async def test_sensors_use_detailed_eta_if_available(self):
"""Test sensor states when last order is not yet delivered."""
# Set-up platform with default mock responses
@ -367,6 +387,21 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNAVAILABLE)
self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE)
async def test_sensors_malformed_delivery_data(self):
"""Test sensor states when the delivery api returns not a list."""
# Setup platform with default responses
await self._setup_platform(use_default_responses=True)
# Change mock responses to empty data and refresh the coordinator
self.picnic_mock().get_deliveries.return_value = {"error": "message"}
await self._coordinator.async_refresh()
# Assert all last-order sensors have STATE_UNAVAILABLE because the delivery info fetch failed
assert self._coordinator.last_update_success is True
self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNKNOWN)
self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNKNOWN)
self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN)
async def test_sensors_malformed_response(self):
"""Test coordinator update fails when API yields ValueError."""
# Setup platform with default responses

View File

@ -68,7 +68,7 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock):
mock_soco.alarmClock = alarm_clock
mock_soco.mute = False
mock_soco.night_mode = True
mock_soco.dialog_mode = True
mock_soco.dialog_level = True
mock_soco.volume = 19
mock_soco.bass = 1
mock_soco.treble = -1