diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 3cfe7a14167..48f4ef82b81 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.55"], + "requirements": ["AIOAladdinConnect==0.1.56"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 94e79009853..e4ac8985ebd 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -22,6 +22,7 @@ from .const import DOMAIN, PRODUCT SCAN_INTERVAL = timedelta(seconds=5) BLEBOX_TO_HVACMODE = { + None: None, 0: HVACMode.OFF, 1: HVACMode.HEAT, 2: HVACMode.COOL, @@ -58,13 +59,15 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn @property def hvac_modes(self): """Return list of supported HVAC modes.""" - return [HVACMode.OFF, self.hvac_mode] + return [HVACMode.OFF, BLEBOX_TO_HVACMODE[self._feature.mode]] @property def hvac_mode(self): """Return the desired HVAC mode.""" if self._feature.is_on is None: return None + if not self._feature.is_on: + return HVACMode.OFF if self._feature.mode is not None: return BLEBOX_TO_HVACMODE[self._feature.mode] return HVACMode.HEAT if self._feature.is_on else HVACMode.OFF diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 46fe45873b4..7b2761b5575 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -3,7 +3,7 @@ "name": "Matter (BETA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/matter", - "requirements": ["python-matter-server==2.0.2"], + "requirements": ["python-matter-server==2.1.0"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 9bc2f0011c0..047d7d9ce1a 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -3,7 +3,7 @@ "name": "OctoPrint", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/octoprint", - "requirements": ["pyoctoprintapi==0.1.9"], + "requirements": ["pyoctoprintapi==0.1.11"], "codeowners": ["@rfleming71"], "zeroconf": ["_octoprint._tcp.local."], "ssdp": [ diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index b5644915d91..294bbbd6e90 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -15,7 +15,7 @@ An overview of the areas and the devices in this smart home: {{ area.name }}: {%- set area_info.printed = true %} {%- endif %} -- {{ device_attr(device, "name") }}{% if device_attr(device, "model") and device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} +- {{ device_attr(device, "name") }}{% if device_attr(device, "model") and (device_attr(device, "model") | string) not in (device_attr(device, "name") | string) %} ({{ device_attr(device, "model") }}){% endif %} {%- endif %} {%- endfor %} {%- endfor %} diff --git a/homeassistant/components/openuv/coordinator.py b/homeassistant/components/openuv/coordinator.py index 7472f213f82..5d0c4bce50a 100644 --- a/homeassistant/components/openuv/coordinator.py +++ b/homeassistant/components/openuv/coordinator.py @@ -6,7 +6,7 @@ from typing import Any, cast from pyopenuv.errors import InvalidApiKeyError, OpenUvError -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer @@ -60,14 +60,4 @@ class OpenUvCoordinator(DataUpdateCoordinator): except OpenUvError as err: raise UpdateFailed(str(err)) from err - # OpenUV uses HTTP 403 to indicate both an invalid API key and an API key that - # has hit its daily/monthly limit; both cases will result in a reauth flow. If - # coordinator update succeeds after a reauth flow has been started, terminate - # it: - if reauth_flow := next( - iter(self._entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})), - None, - ): - self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"]) - return cast(dict[str, Any], data["result"]) diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index ef367b94dac..ae30a36022e 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -3,7 +3,7 @@ "name": "OpenUV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", - "requirements": ["pyopenuv==2023.01.0"], + "requirements": ["pyopenuv==2023.02.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyopenuv"], diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 0519960945e..64d5b492e3b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.4.0"], + "requirements": ["reolink-aio==0.4.2"], "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 5108a167552..2fbd8ef211d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -587,7 +587,7 @@ class SensorEntity(Entity): numerical_value: int | float | Decimal if not isinstance(value, (int, float, Decimal)): try: - if isinstance(value, str) and "." not in value: + if isinstance(value, str) and "." not in value and "e" not in value: numerical_value = int(value) else: numerical_value = float(value) # type:ignore[arg-type] diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 38434c15a1c..60dc8f54ed7 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -5,15 +5,17 @@ from collections import deque from collections.abc import Callable import contextlib from datetime import datetime, timedelta +from enum import Enum import logging import statistics -from typing import Any, Literal, cast +from typing import Any, Literal, TypeVar, cast import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.recorder import get_instance, history -from homeassistant.components.sensor import ( +from homeassistant.components.sensor import ( # pylint: disable=hass-deprecated-import + DEVICE_CLASS_STATE_CLASSES, PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, @@ -144,7 +146,7 @@ STATS_DATETIME = { } # Statistics which retain the unit of the source entity -STAT_NUMERIC_RETAIN_UNIT = { +STATS_NUMERIC_RETAIN_UNIT = { STAT_AVERAGE_LINEAR, STAT_AVERAGE_STEP, STAT_AVERAGE_TIMELESS, @@ -166,7 +168,7 @@ STAT_NUMERIC_RETAIN_UNIT = { } # Statistics which produce percentage ratio from binary_sensor source entity -STAT_BINARY_PERCENTAGE = { +STATS_BINARY_PERCENTAGE = { STAT_AVERAGE_STEP, STAT_AVERAGE_TIMELESS, STAT_MEAN, @@ -296,15 +298,9 @@ class StatisticsSensor(SensorEntity): self.ages: deque[datetime] = deque(maxlen=self._samples_max_buffer_size) self.attributes: dict[str, StateType] = {} - self._state_characteristic_fn: Callable[[], StateType | datetime] - if self.is_binary: - self._state_characteristic_fn = getattr( - self, f"_stat_binary_{self._state_characteristic}" - ) - else: - self._state_characteristic_fn = getattr( - self, f"_stat_{self._state_characteristic}" - ) + self._state_characteristic_fn: Callable[ + [], StateType | datetime + ] = self._callable_characteristic_fn(self._state_characteristic) self._update_listener: CALLBACK_TYPE | None = None @@ -368,11 +364,11 @@ class StatisticsSensor(SensorEntity): def _derive_unit_of_measurement(self, new_state: State) -> str | None: base_unit: str | None = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) unit: str | None - if self.is_binary and self._state_characteristic in STAT_BINARY_PERCENTAGE: + if self.is_binary and self._state_characteristic in STATS_BINARY_PERCENTAGE: unit = PERCENTAGE elif not base_unit: unit = None - elif self._state_characteristic in STAT_NUMERIC_RETAIN_UNIT: + elif self._state_characteristic in STATS_NUMERIC_RETAIN_UNIT: unit = base_unit elif self._state_characteristic in STATS_NOT_A_NUMBER: unit = None @@ -393,11 +389,24 @@ class StatisticsSensor(SensorEntity): @property def device_class(self) -> SensorDeviceClass | None: """Return the class of this device.""" - if self._state_characteristic in STAT_NUMERIC_RETAIN_UNIT: - _state = self.hass.states.get(self._source_entity_id) - return None if _state is None else _state.attributes.get(ATTR_DEVICE_CLASS) if self._state_characteristic in STATS_DATETIME: return SensorDeviceClass.TIMESTAMP + if self._state_characteristic in STATS_NUMERIC_RETAIN_UNIT: + source_state = self.hass.states.get(self._source_entity_id) + if source_state is None: + return None + source_device_class = source_state.attributes.get(ATTR_DEVICE_CLASS) + if source_device_class is None: + return None + sensor_device_class = try_parse_enum(SensorDeviceClass, source_device_class) + if sensor_device_class is None: + return None + sensor_state_classes = DEVICE_CLASS_STATE_CLASSES.get( + sensor_device_class, set() + ) + if SensorStateClass.MEASUREMENT not in sensor_state_classes: + return None + return sensor_device_class return None @property @@ -472,8 +481,8 @@ class StatisticsSensor(SensorEntity): if timestamp := self._next_to_purge_timestamp(): _LOGGER.debug("%s: scheduling update at %s", self.entity_id, timestamp) if self._update_listener: - self._update_listener() - self._update_listener = None + self._update_listener() # pragma: no cover + self._update_listener = None # pragma: no cover @callback def _scheduled_update(now: datetime) -> None: @@ -563,6 +572,18 @@ class StatisticsSensor(SensorEntity): value = int(value) self._value = value + def _callable_characteristic_fn( + self, characteristic: str + ) -> Callable[[], StateType | datetime]: + """Return the function callable of one characteristic function.""" + function: Callable[[], StateType | datetime] = getattr( + self, + f"_stat_binary_{characteristic}" + if self.is_binary + else f"_stat_{characteristic}", + ) + return function + # Statistics for numeric sensor def _stat_average_linear(self) -> StateType: @@ -748,3 +769,16 @@ class StatisticsSensor(SensorEntity): if len(self.states) > 0: return 100.0 / len(self.states) * self.states.count(True) return None + + +_EnumT = TypeVar("_EnumT", bound=Enum) + + +def try_parse_enum(cls: type[_EnumT], value: Any) -> _EnumT | None: + """Try to parse the value into an Enum. + + Return None if parsing fails. + """ + with contextlib.suppress(ValueError): + return cls(value) + return None diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index cb3c88532d9..770d342f52c 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.26.12"], + "requirements": ["pyTibber==0.26.13"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index b026dd0ce32..9c26a0319f7 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -119,11 +119,12 @@ SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = ( key="DispenseLevel", name="Detergent Level", translation_key="whirlpool_tank", + entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, options=list(TANK_FILL.values()), - value_fn=lambda WasherDryer: TANK_FILL[ + value_fn=lambda WasherDryer: TANK_FILL.get( WasherDryer.get_attribute("WashCavity_OpStatusBulkDispense1Level") - ], + ), ), ) @@ -265,6 +266,7 @@ class WasherDryerTimeClass(RestoreSensor): async def async_will_remove_from_hass(self) -> None: """Close Whrilpool Appliance sockets before removing.""" + self._wd.unregister_attr_callback(self.update_from_latest_data) await self._wd.disconnect() @property diff --git a/homeassistant/const.py b/homeassistant/const.py index acad89bd43f..4d7d066a890 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8ad9a144a1a..ac49c02cc53 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.8.5 +orjson==3.8.6 paho-mqtt==1.6.1 pillow==9.4.0 pip>=21.0,<22.4 diff --git a/pyproject.toml b/pyproject.toml index 092fdb74079..1103b78dc77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.4" +version = "2023.2.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -44,7 +44,7 @@ dependencies = [ "cryptography==39.0.1", # pyOpenSSL 23.0.0 is required to work with cryptography 39+ "pyOpenSSL==23.0.0", - "orjson==3.8.5", + "orjson==3.8.6", "pip>=21.0,<22.4", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index 7d05e1bb2e7..c2404d880b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ lru-dict==1.1.8 PyJWT==2.5.0 cryptography==39.0.1 pyOpenSSL==23.0.0 -orjson==3.8.5 +orjson==3.8.6 pip>=21.0,<22.4 python-slugify==4.0.1 pyyaml==6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 84ece70f753..125ff841354 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.55 +AIOAladdinConnect==0.1.56 # homeassistant.components.adax Adax-local==0.1.5 @@ -1470,7 +1470,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.12 +pyTibber==0.26.13 # homeassistant.components.dlink pyW215==0.7.0 @@ -1828,13 +1828,13 @@ pynzbgetapi==0.2.0 pyobihai==1.3.2 # homeassistant.components.octoprint -pyoctoprintapi==0.1.9 +pyoctoprintapi==0.1.11 # homeassistant.components.ombi pyombi==0.1.10 # homeassistant.components.openuv -pyopenuv==2023.01.0 +pyopenuv==2023.02.0 # homeassistant.components.opnsense pyopnsense==0.2.0 @@ -2072,7 +2072,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==2.0.2 +python-matter-server==2.1.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.4.0 +reolink-aio==0.4.2 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3345692e41c..68557b36f5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.55 +AIOAladdinConnect==0.1.56 # homeassistant.components.adax Adax-local==0.1.5 @@ -1073,7 +1073,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.12 +pyTibber==0.26.13 # homeassistant.components.dlink pyW215==0.7.0 @@ -1317,10 +1317,10 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.octoprint -pyoctoprintapi==0.1.9 +pyoctoprintapi==0.1.11 # homeassistant.components.openuv -pyopenuv==2023.01.0 +pyopenuv==2023.02.0 # homeassistant.components.opnsense pyopnsense==0.2.0 @@ -1468,7 +1468,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==2.0.2 +python-matter-server==2.1.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.4.0 +reolink-aio==0.4.2 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index ad66bb7f6a9..c8592c84849 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch +from matter_server.common.const import SCHEMA_VERSION from matter_server.common.models.server_information import ServerInfo import pytest @@ -45,6 +46,7 @@ async def matter_client_fixture() -> AsyncGenerator[MagicMock, None]: sdk_version="2022.11.1", wifi_credentials_set=True, thread_credentials_set=True, + min_supported_schema_version=SCHEMA_VERSION, ) yield client diff --git a/tests/components/matter/fixtures/config_entry_diagnostics.json b/tests/components/matter/fixtures/config_entry_diagnostics.json index 13a4e7d26a5..8c7b0960fc8 100644 --- a/tests/components/matter/fixtures/config_entry_diagnostics.json +++ b/tests/components/matter/fixtures/config_entry_diagnostics.json @@ -5,7 +5,8 @@ "schema_version": 1, "sdk_version": "2022.12.0", "wifi_credentials_set": true, - "thread_credentials_set": false + "thread_credentials_set": false, + "min_supported_schema_version": 1 }, "nodes": [ { diff --git a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json index 8f798a50467..bbe7cf1f2c6 100644 --- a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json +++ b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json @@ -6,7 +6,8 @@ "schema_version": 1, "sdk_version": "2022.12.0", "wifi_credentials_set": true, - "thread_credentials_set": false + "thread_credentials_set": false, + "min_supported_schema_version": 1 }, "nodes": [ { diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index b64f3322895..14d2015ace2 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -67,14 +67,21 @@ async def test_default_prompt(hass, mock_init_component): device_reg.async_update_device( device.id, disabled_by=device_registry.DeviceEntryDisabler.USER ) - device = device_reg.async_get_or_create( + device_reg.async_get_or_create( config_entry_id="1234", connections={("test", "9876-no-name")}, manufacturer="Test Manufacturer NoName", model="Test Model NoName", suggested_area="Test Area 2", ) - + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "9876-integer-values")}, + name=1, + manufacturer=2, + model=3, + suggested_area="Test Area 2", + ) with patch("openai.Completion.create") as mock_create: result = await conversation.async_converse(hass, "hello", None, Context()) @@ -93,6 +100,7 @@ Test Area 2: - Test Device 2 - Test Device 3 (Test Model 3A) - Test Device 4 +- 1 (3) Answer the users questions about the world truthfully. diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index b43c63f015c..f63d10bc76a 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1548,6 +1548,7 @@ async def test_non_numeric_validation_raise( [ (13, "13"), (17.50, "17.5"), + ("1e-05", "1e-05"), (Decimal(18.50), "18.5"), ("19.70", "19.70"), (None, STATE_UNKNOWN), diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 7f68ae68973..148ae87b801 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, + UnitOfEnergy, UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -250,6 +251,63 @@ async def test_sensor_source_with_force_update(hass: HomeAssistant): assert state_force.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) +async def test_sampling_boundaries_given(hass: HomeAssistant): + """Test if either sampling_size or max_age are given.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_boundaries_none", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + }, + { + "platform": "statistics", + "name": "test_boundaries_size", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "sampling_size": 20, + }, + { + "platform": "statistics", + "name": "test_boundaries_age", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "max_age": {"minutes": 4}, + }, + { + "platform": "statistics", + "name": "test_boundaries_both", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "sampling_size": 20, + "max_age": {"minutes": 4}, + }, + ] + }, + ) + await hass.async_block_till_done() + + hass.states.async_set( + "sensor.test_monitored", + str(VALUES_NUMERIC[0]), + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_boundaries_none") + assert state is None + state = hass.states.get("sensor.test_boundaries_size") + assert state is not None + state = hass.states.get("sensor.test_boundaries_age") + assert state is not None + state = hass.states.get("sensor.test_boundaries_both") + assert state is not None + + async def test_sampling_size_reduced(hass: HomeAssistant): """Test limited buffer size.""" assert await async_setup_component( @@ -514,9 +572,9 @@ async def test_device_class(hass: HomeAssistant): { "sensor": [ { - # Device class is carried over from source sensor for characteristics with same unit + # Device class is carried over from source sensor for characteristics which retain unit "platform": "statistics", - "name": "test_source_class", + "name": "test_retain_unit", "entity_id": "sensor.test_monitored", "state_characteristic": "mean", "sampling_size": 20, @@ -537,6 +595,14 @@ async def test_device_class(hass: HomeAssistant): "state_characteristic": "datetime_oldest", "sampling_size": 20, }, + { + # Device class is set to None for any source sensor with TOTAL state class + "platform": "statistics", + "name": "test_source_class_total", + "entity_id": "sensor.test_monitored_total", + "state_characteristic": "mean", + "sampling_size": 20, + }, ] }, ) @@ -549,11 +615,21 @@ async def test_device_class(hass: HomeAssistant): { ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + ) + hass.states.async_set( + "sensor.test_monitored_total", + str(value), + { + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, }, ) await hass.async_block_till_done() - state = hass.states.get("sensor.test_source_class") + state = hass.states.get("sensor.test_retain_unit") assert state is not None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE state = hass.states.get("sensor.test_none") @@ -562,6 +638,9 @@ async def test_device_class(hass: HomeAssistant): state = hass.states.get("sensor.test_timestamp") assert state is not None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + state = hass.states.get("sensor.test_source_class_total") + assert state is not None + assert state.attributes.get(ATTR_DEVICE_CLASS) is None async def test_state_class(hass: HomeAssistant): @@ -572,6 +651,15 @@ async def test_state_class(hass: HomeAssistant): { "sensor": [ { + # State class is None for datetime characteristics + "platform": "statistics", + "name": "test_nan", + "entity_id": "sensor.test_monitored", + "state_characteristic": "datetime_oldest", + "sampling_size": 20, + }, + { + # State class is MEASUREMENT for all other characteristics "platform": "statistics", "name": "test_normal", "entity_id": "sensor.test_monitored", @@ -579,10 +667,12 @@ async def test_state_class(hass: HomeAssistant): "sampling_size": 20, }, { + # State class is MEASUREMENT, even when the source sensor + # is of state class TOTAL "platform": "statistics", - "name": "test_nan", - "entity_id": "sensor.test_monitored", - "state_characteristic": "datetime_oldest", + "name": "test_total", + "entity_id": "sensor.test_monitored_total", + "state_characteristic": "count", "sampling_size": 20, }, ] @@ -596,14 +686,28 @@ async def test_state_class(hass: HomeAssistant): str(value), {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) + hass.states.async_set( + "sensor.test_monitored_total", + str(value), + { + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + }, + ) await hass.async_block_till_done() - state = hass.states.get("sensor.test_normal") - assert state is not None - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT state = hass.states.get("sensor.test_nan") assert state is not None assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get("sensor.test_normal") + assert state is not None + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + state = hass.states.get("sensor.test_monitored_total") + assert state is not None + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + state = hass.states.get("sensor.test_total") + assert state is not None + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT async def test_unitless_source_sensor(hass: HomeAssistant): diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index dd06c2d768f..1671768fdb6 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -49,6 +49,21 @@ def fixture_mock_appliances_manager_api(): yield mock_appliances_manager +@pytest.fixture(name="mock_appliances_manager_laundry_api") +def fixture_mock_appliances_manager_laundry_api(): + """Set up AppliancesManager fixture.""" + with mock.patch( + "homeassistant.components.whirlpool.AppliancesManager" + ) as mock_appliances_manager: + mock_appliances_manager.return_value.fetch_appliances = AsyncMock() + mock_appliances_manager.return_value.aircons = None + mock_appliances_manager.return_value.washer_dryers = [ + {"SAID": MOCK_SAID3, "NAME": "washer"}, + {"SAID": MOCK_SAID4, "NAME": "dryer"}, + ] + yield mock_appliances_manager + + @pytest.fixture(name="mock_backend_selector_api") def fixture_mock_backend_selector_api(): """Set up BackendSelector fixture.""" @@ -115,8 +130,6 @@ def side_effect_function(*args, **kwargs): return "0" if args[0] == "WashCavity_OpStatusBulkDispense1Level": return "3" - if args[0] == "Cavity_TimeStatusEstTimeRemaining": - return "4000" def get_sensor_mock(said): @@ -141,13 +154,13 @@ def get_sensor_mock(said): @pytest.fixture(name="mock_sensor1_api", autouse=False) -def fixture_mock_sensor1_api(mock_auth_api, mock_appliances_manager_api): +def fixture_mock_sensor1_api(mock_auth_api, mock_appliances_manager_laundry_api): """Set up sensor API fixture.""" yield get_sensor_mock(MOCK_SAID3) @pytest.fixture(name="mock_sensor2_api", autouse=False) -def fixture_mock_sensor2_api(mock_auth_api, mock_appliances_manager_api): +def fixture_mock_sensor2_api(mock_auth_api, mock_appliances_manager_laundry_api): """Set up sensor API fixture.""" yield get_sensor_mock(MOCK_SAID4) @@ -161,5 +174,7 @@ def fixture_mock_sensor_api_instances(mock_sensor1_api, mock_sensor2_api): mock_sensor_api.side_effect = [ mock_sensor1_api, mock_sensor2_api, + mock_sensor1_api, + mock_sensor2_api, ] yield mock_sensor_api diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index b8801bd4fd5..eef13c08dc9 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -17,7 +17,7 @@ async def update_sensor_state( hass: HomeAssistant, entity_id: str, mock_sensor_api_instance: MagicMock, -): +) -> None: """Simulate an update trigger from the API.""" for call in mock_sensor_api_instance.register_attr_callback.call_args_list: @@ -44,7 +44,7 @@ async def test_dryer_sensor_values( hass: HomeAssistant, mock_sensor_api_instances: MagicMock, mock_sensor2_api: MagicMock, -): +) -> None: """Test the sensor value callbacks.""" hass.state = CoreState.not_running thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) @@ -108,7 +108,7 @@ async def test_washer_sensor_values( hass: HomeAssistant, mock_sensor_api_instances: MagicMock, mock_sensor1_api: MagicMock, -): +) -> None: """Test the sensor value callbacks.""" hass.state = CoreState.not_running thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) @@ -147,6 +147,21 @@ async def test_washer_sensor_values( assert state.state == thetimestamp.isoformat() state_id = f"{entity_id.split('_')[0]}_detergent_level" + registry = entity_registry.async_get(hass) + entry = registry.async_get(state_id) + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + + update_entry = registry.async_update_entity(entry.entity_id, disabled_by=None) + await hass.async_block_till_done() + + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get(state_id) + assert state is None + + await hass.config_entries.async_reload(entry.config_entry_id) state = hass.states.get(state_id) assert state is not None assert state.state == "50" @@ -253,7 +268,7 @@ async def test_washer_sensor_values( async def test_restore_state( hass: HomeAssistant, mock_sensor_api_instances: MagicMock, -): +) -> None: """Test sensor restore state.""" # Home assistant is not running yet hass.state = CoreState.not_running @@ -288,7 +303,7 @@ async def test_callback( hass: HomeAssistant, mock_sensor_api_instances: MagicMock, mock_sensor1_api: MagicMock, -): +) -> None: """Test callback timestamp callback function.""" hass.state = CoreState.not_running thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) @@ -314,9 +329,9 @@ async def test_callback( # restore from cache state = hass.states.get("sensor.washer_end_time") assert state.state == thetimestamp.isoformat() - callback = mock_sensor1_api.register_attr_callback.call_args_list[2][0][0] + callback = mock_sensor1_api.register_attr_callback.call_args_list[1][0][0] callback() - # await hass.async_block_till_done() + state = hass.states.get("sensor.washer_end_time") assert state.state == thetimestamp.isoformat() mock_sensor1_api.get_machine_state.return_value = MachineState.RunningMainCycle