Fix picnic sensor time unit (#62437)

This commit is contained in:
corneyl 2021-12-27 17:44:45 +01:00 committed by GitHub
parent dc3f21dd1e
commit b0704c190f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 58 additions and 21 deletions

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 SensorDeviceClass, SensorEntityDescription from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription
from homeassistant.const import CURRENCY_EURO from homeassistant.const import CURRENCY_EURO
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=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.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=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.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

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

@ -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
@ -11,7 +12,12 @@ from homeassistant import config_entries
from homeassistant.components.picnic import const from homeassistant.components.picnic import const
from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, SENSOR_TYPES from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, SENSOR_TYPES
from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import CONF_ACCESS_TOKEN, CURRENCY_EURO, STATE_UNAVAILABLE from homeassistant.const import (
CONF_ACCESS_TOKEN,
CURRENCY_EURO,
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
@ -99,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 = {
@ -277,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):
@ -300,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"
) )
@ -308,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