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", "name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [ "requirements": [
"home-assistant-frontend==20211220.0" "home-assistant-frontend==20211227.0"
], ],
"dependencies": [ "dependencies": [
"api", "api",

View File

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

View File

@ -145,6 +145,7 @@ class HueBaseEntity(Entity):
if self.device.product_data.certified: if self.device.product_data.certified:
# certified products report their state correctly # certified products report their state correctly
self._ignore_availability = False self._ignore_availability = False
return
# some (3th party) Hue lights report their connection status incorrectly # some (3th party) Hue lights report their connection status incorrectly
# causing the zigbee availability to report as disconnected while in fact # causing the zigbee availability to report as disconnected while in fact
# it can be controlled. Although this is in fact something the device manufacturer # 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 collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from typing import Any, Literal from typing import Any, Literal
from homeassistant.components.sensor import SensorEntityDescription from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.const import CURRENCY_EURO, DEVICE_CLASS_TIMESTAMP from homeassistant.const import CURRENCY_EURO, DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
DOMAIN = "picnic" DOMAIN = "picnic"
@ -42,7 +44,7 @@ class PicnicRequiredKeysMixin:
"""Mixin for required keys.""" """Mixin for required keys."""
data_type: Literal["cart_data", "slot_data", "last_order_data"] data_type: Literal["cart_data", "slot_data", "last_order_data"]
value_fn: Callable[[Any], StateType] value_fn: Callable[[Any], StateType | datetime]
@dataclass @dataclass
@ -73,7 +75,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:calendar-start", icon="mdi:calendar-start",
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
data_type="slot_data", 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( PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_END, key=SENSOR_SELECTED_SLOT_END,
@ -81,7 +83,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:calendar-end", icon="mdi:calendar-end",
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
data_type="slot_data", 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( PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME,
@ -89,7 +91,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:clock-alert-outline", icon="mdi:clock-alert-outline",
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
data_type="slot_data", 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( PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE,
@ -108,14 +110,18 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
device_class=DEVICE_CLASS_TIMESTAMP, device_class=DEVICE_CLASS_TIMESTAMP,
icon="mdi:calendar-start", icon="mdi:calendar-start",
data_type="last_order_data", 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( PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_SLOT_END, key=SENSOR_LAST_ORDER_SLOT_END,
device_class=DEVICE_CLASS_TIMESTAMP, device_class=DEVICE_CLASS_TIMESTAMP,
icon="mdi:calendar-end", icon="mdi:calendar-end",
data_type="last_order_data", 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( PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_STATUS, key=SENSOR_LAST_ORDER_STATUS,
@ -129,7 +135,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:clock-start", icon="mdi:clock-start",
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
data_type="last_order_data", 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( PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_ETA_END, key=SENSOR_LAST_ORDER_ETA_END,
@ -137,7 +145,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:clock-end", icon="mdi:clock-end",
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
data_type="last_order_data", 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( PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_DELIVERY_TIME, key=SENSOR_LAST_ORDER_DELIVERY_TIME,
@ -145,7 +155,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
icon="mdi:timeline-clock", icon="mdi:timeline-clock",
entity_registry_enabled_default=True, entity_registry_enabled_default=True,
data_type="last_order_data", 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( PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_TOTAL_PRICE, 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 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 # Fetch from the API and pre-process the data
cart = self.picnic_api_client.get_cart() 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.") raise UpdateFailed("API response doesn't contain expected data.")
last_order = self._get_last_order()
slot_data = self._get_slot_data(cart) slot_data = self._get_slot_data(cart)
return { return {
@ -102,11 +102,12 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator):
"""Get data of the last order from the list of deliveries.""" """Get data of the last order from the list of deliveries."""
# Get the deliveries # Get the deliveries
deliveries = self.picnic_api_client.get_deliveries(summary=True) deliveries = self.picnic_api_client.get_deliveries(summary=True)
if not deliveries:
return {}
# Determine the last order # Determine the last order and return an empty dict if there is none
try:
last_order = copy.deepcopy(deliveries[0]) last_order = copy.deepcopy(deliveries[0])
except KeyError:
return {}
# Get the position details if the order is not delivered yet # Get the position details if the order is not delivered yet
delivery_position = {} delivery_position = {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
"domain": "tuya", "domain": "tuya",
"name": "Tuya", "name": "Tuya",
"documentation": "https://www.home-assistant.io/integrations/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"], "dependencies": ["ffmpeg"],
"codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"], "codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"],
"config_flow": true, "config_flow": true,

View File

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

View File

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

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021 MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12 MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "5" PATCH_VERSION: Final = "6"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) 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 cryptography==35.0.0
emoji==1.5.0 emoji==1.5.0
hass-nabucasa==0.50.0 hass-nabucasa==0.50.0
home-assistant-frontend==20211220.0 home-assistant-frontend==20211227.0
httpx==0.21.0 httpx==0.21.0
ifaddr==0.1.7 ifaddr==0.1.7
jinja2==3.0.3 jinja2==3.0.3
@ -33,7 +33,7 @@ sqlalchemy==1.4.27
voluptuous-serialize==2.5.0 voluptuous-serialize==2.5.0
voluptuous==0.12.2 voluptuous==0.12.2
yarl==1.6.3 yarl==1.6.3
zeroconf==0.37.0 zeroconf==0.38.1
pycryptodome>=3.6.6 pycryptodome>=3.6.6

View File

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

View File

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

View File

@ -1,6 +1,7 @@
"""The tests for the Picnic sensor platform.""" """The tests for the Picnic sensor platform."""
import copy import copy
from datetime import timedelta from datetime import timedelta
from typing import Dict
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
@ -15,6 +16,7 @@ from homeassistant.const import (
CURRENCY_EURO, CURRENCY_EURO,
DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_TIMESTAMP,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN,
) )
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.util import dt from homeassistant.util import dt
@ -103,6 +105,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
# Patch the api client # Patch the api client
self.picnic_patcher = patch("homeassistant.components.picnic.PicnicAPI") self.picnic_patcher = patch("homeassistant.components.picnic.PicnicAPI")
self.picnic_mock = self.picnic_patcher.start() self.picnic_mock = self.picnic_patcher.start()
self.picnic_mock().session.auth_token = "3q29fpwhulzes"
# Add a config entry and setup the integration # Add a config entry and setup the integration
config_data = { config_data = {
@ -281,13 +284,11 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
await self._setup_platform() await self._setup_platform()
# Assert sensors are unknown # Assert sensors are unknown
self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNAVAILABLE) self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNKNOWN)
self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNAVAILABLE) 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( self._assert_sensor(
"sensor.picnic_selected_slot_max_order_time", STATE_UNAVAILABLE "sensor.picnic_selected_slot_min_order_value", STATE_UNKNOWN
)
self._assert_sensor(
"sensor.picnic_selected_slot_min_order_value", STATE_UNAVAILABLE
) )
async def test_sensors_last_order_in_future(self): async def test_sensors_last_order_in_future(self):
@ -304,7 +305,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
await self._setup_platform() await self._setup_platform()
# Assert delivery time is not available, but eta is # 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( self._assert_sensor(
"sensor.picnic_last_order_eta_start", "2021-02-26T19:54:00+00:00" "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" "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): async def test_sensors_use_detailed_eta_if_available(self):
"""Test sensor states when last order is not yet delivered.""" """Test sensor states when last order is not yet delivered."""
# Set-up platform with default mock responses # 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_eta_end", STATE_UNAVAILABLE)
self._assert_sensor("sensor.picnic_last_order_delivery_time", 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): async def test_sensors_malformed_response(self):
"""Test coordinator update fails when API yields ValueError.""" """Test coordinator update fails when API yields ValueError."""
# Setup platform with default responses # 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.alarmClock = alarm_clock
mock_soco.mute = False mock_soco.mute = False
mock_soco.night_mode = True mock_soco.night_mode = True
mock_soco.dialog_mode = True mock_soco.dialog_level = True
mock_soco.volume = 19 mock_soco.volume = 19
mock_soco.bass = 1 mock_soco.bass = 1
mock_soco.treble = -1 mock_soco.treble = -1