diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6ae1709e418..a2c7e49f6e6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -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", diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 30ea244bac9..843ebfee87a 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -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: diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 69b299d574f..8253d4ffbef 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -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 diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index 228983d8189..be2b403b3b8 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 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, diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index bcd4e79a098..71a6559975c 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -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 = {} 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/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 907026fd77e..ad5857aa630 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -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": [ { diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 7fcf456b658..bd9041d3344 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -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", diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 00006ab4e90..d4fce01fd78 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -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."], diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 8cd2abcf2b2..ffc0d08a1b4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -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 diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index e92263991ab..ad98523258a 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -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" diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index c48771b85be..1b8772a36df 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -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, diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index 49a5b6187e6..f5713c51680 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -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" } diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 338456ca576..16a8a8ff26e 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -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", diff --git a/homeassistant/const.py b/homeassistant/const.py index 814089c45b3..f20d78f9224 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5ea2198a3aa..19b764ee32f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index f19a9aa6e6c..41318a26a28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4bee163175..f2e191885c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index b0d12f1f080..83653945d8c 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 @@ -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 diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f7f8d67589f..18c5366d4ae 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -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