From b0704c190f6d1096efb0889c7c307820c78071d5 Mon Sep 17 00:00:00 2001 From: corneyl Date: Mon, 27 Dec 2021 17:44:45 +0100 Subject: [PATCH] Fix picnic sensor time unit (#62437) --- homeassistant/components/picnic/const.py | 30 ++++++++++++----- homeassistant/components/picnic/sensor.py | 9 ++--- tests/components/picnic/test_sensor.py | 40 ++++++++++++++++++----- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index 59b969236c4..da86b00bb22 100644 --- a/homeassistant/components/picnic/const.py +++ b/homeassistant/components/picnic/const.py @@ -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 SensorDeviceClass, SensorEntityDescription from homeassistant.const import CURRENCY_EURO 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=SensorDeviceClass.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=SensorDeviceClass.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, diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 95612e7b272..8e64283c5f0 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -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: diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 4773206f5cf..f1882bfa098 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -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 @@ -11,7 +12,12 @@ from homeassistant import config_entries from homeassistant.components.picnic import const from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, SENSOR_TYPES 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.util import dt @@ -99,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 = { @@ -277,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): @@ -300,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" ) @@ -308,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