diff --git a/homeassistant/components/smhi/strings.json b/homeassistant/components/smhi/strings.json index 17f6fc6799f52..50fb0c4c2c998 100644 --- a/homeassistant/components/smhi/strings.json +++ b/homeassistant/components/smhi/strings.json @@ -45,8 +45,8 @@ "name": "Fuel drying", "state": { "dry": "Dry", - "extremely_dry": "Extemely dry", - "moderate_wet": "Moderate wet", + "extremely_dry": "Extremely dry", + "moderate_wet": "Moderately wet", "very_dry": "Very dry", "very_wet": "Very wet", "wet": "Wet" diff --git a/homeassistant/components/systemmonitor/binary_sensor.py b/homeassistant/components/systemmonitor/binary_sensor.py index ad84e727129ad..68665b2eecf00 100644 --- a/homeassistant/components/systemmonitor/binary_sensor.py +++ b/homeassistant/components/systemmonitor/binary_sensor.py @@ -66,11 +66,11 @@ def get_process(entity: SystemMonitorSensor) -> bool: class SysMonitorBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes System Monitor binary sensor entities.""" - value_fn: Callable[[SystemMonitorSensor], bool] + value_fn: Callable[[SystemMonitorSensor], bool | None] add_to_update: Callable[[SystemMonitorSensor], tuple[str, str]] -SENSOR_TYPES: tuple[SysMonitorBinarySensorEntityDescription, ...] = ( +PROCESS_TYPES: tuple[SysMonitorBinarySensorEntityDescription, ...] = ( SysMonitorBinarySensorEntityDescription( key="binary_process", translation_key="process", @@ -81,6 +81,20 @@ class SysMonitorBinarySensorEntityDescription(BinarySensorEntityDescription): ), ) +BINARY_SENSOR_TYPES: tuple[SysMonitorBinarySensorEntityDescription, ...] = ( + SysMonitorBinarySensorEntityDescription( + key="battery_plugged", + value_fn=( + lambda entity: entity.coordinator.data.battery.power_plugged + if entity.coordinator.data.battery + else None + ), + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + add_to_update=lambda entity: ("battery", ""), + entity_registry_enabled_default=False, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -90,18 +104,30 @@ async def async_setup_entry( """Set up System Monitor binary sensors based on a config entry.""" coordinator = entry.runtime_data.coordinator - async_add_entities( + entities: list[SystemMonitorSensor] = [] + + entities.extend( SystemMonitorSensor( coordinator, sensor_description, entry.entry_id, argument, ) - for sensor_description in SENSOR_TYPES + for sensor_description in PROCESS_TYPES for argument in entry.options.get(BINARY_SENSOR_DOMAIN, {}).get( CONF_PROCESS, [] ) ) + entities.extend( + SystemMonitorSensor( + coordinator, + sensor_description, + entry.entry_id, + "", + ) + for sensor_description in BINARY_SENSOR_TYPES + ) + async_add_entities(entities) class SystemMonitorSensor( diff --git a/homeassistant/components/systemmonitor/coordinator.py b/homeassistant/components/systemmonitor/coordinator.py index eeccddf949fe7..9478ab8a73868 100644 --- a/homeassistant/components/systemmonitor/coordinator.py +++ b/homeassistant/components/systemmonitor/coordinator.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from psutil import Process -from psutil._common import sdiskusage, shwtemp, snetio, snicaddr, sswap +from psutil._common import sbattery, sdiskusage, shwtemp, snetio, snicaddr, sswap import psutil_home_assistant as ha_psutil from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -22,6 +22,7 @@ if TYPE_CHECKING: from . import SystemMonitorConfigEntry +from .util import read_fan_speed _LOGGER = logging.getLogger(__name__) @@ -31,9 +32,11 @@ class SensorData: """Sensor data.""" addresses: dict[str, list[snicaddr]] + battery: sbattery | None boot_time: datetime cpu_percent: float | None disk_usage: dict[str, sdiskusage] + fan_speed: dict[str, int] io_counters: dict[str, snetio] load: tuple[float, float, float] memory: VirtualMemory @@ -50,17 +53,23 @@ def as_dict(self) -> dict[str, Any]: disk_usage = None if self.disk_usage: disk_usage = {k: str(v) for k, v in self.disk_usage.items()} + fan_speed = None + if self.fan_speed: + fan_speed = {k: str(v) for k, v in self.fan_speed.items()} io_counters = None if self.io_counters: io_counters = {k: str(v) for k, v in self.io_counters.items()} temperatures = None if self.temperatures: temperatures = {k: str(v) for k, v in self.temperatures.items()} + return { "addresses": addresses, + "battery": str(self.battery), "boot_time": str(self.boot_time), "cpu_percent": str(self.cpu_percent), "disk_usage": disk_usage, + "fan_speed": fan_speed, "io_counters": io_counters, "load": str(self.load), "memory": str(self.memory), @@ -125,8 +134,10 @@ def set_subscribers_tuples( return { **_disk_defaults, ("addresses", ""): set(), + ("battery", ""): set(), ("boot", ""): set(), ("cpu_percent", ""): set(), + ("fan_speed", ""): set(), ("io_counters", ""): set(), ("load", ""): set(), ("memory", ""): set(), @@ -154,9 +165,11 @@ async def _async_update_data(self) -> SensorData: self._initial_update = False return SensorData( addresses=_data["addresses"], + battery=_data["battery"], boot_time=_data["boot_time"], cpu_percent=cpu_percent, disk_usage=_data["disks"], + fan_speed=_data["fan_speed"], io_counters=_data["io_counters"], load=load, memory=_data["memory"], @@ -255,10 +268,29 @@ def update_data(self) -> dict[str, Any]: except AttributeError: _LOGGER.debug("OS does not provide temperature sensors") + fan_speed: dict[str, int] = {} + if self.update_subscribers[("fan_speed", "")] or self._initial_update: + try: + fan_sensors = self._psutil.sensors_fans() + fan_speed = read_fan_speed(fan_sensors) + _LOGGER.debug("fan_speed: %s", fan_speed) + except AttributeError: + _LOGGER.debug("OS does not provide fan sensors") + + battery: sbattery | None = None + if self.update_subscribers[("battery", "")] or self._initial_update: + try: + battery = self._psutil.sensors_battery() + _LOGGER.debug("battery: %s", battery) + except AttributeError: + _LOGGER.debug("OS does not provide battery sensors") + return { "addresses": addresses, + "battery": battery, "boot_time": self.boot_time, "disks": disks, + "fan_speed": fan_speed, "io_counters": io_counters, "memory": memory, "process_fds": process_fds, diff --git a/homeassistant/components/systemmonitor/icons.json b/homeassistant/components/systemmonitor/icons.json index b0ea54acc9834..7e8807917379a 100644 --- a/homeassistant/components/systemmonitor/icons.json +++ b/homeassistant/components/systemmonitor/icons.json @@ -1,6 +1,9 @@ { "entity": { "sensor": { + "battery_empty": { + "default": "mdi:battery-clock" + }, "disk_free": { "default": "mdi:harddisk" }, @@ -10,6 +13,9 @@ "disk_use_percent": { "default": "mdi:harddisk" }, + "fan_speed": { + "default": "mdi:fan" + }, "ipv4_address": { "default": "mdi:ip-network" }, diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 403b17ae5fcda..74401d5b59f6c 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Callable import contextlib from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta from functools import lru_cache import ipaddress import logging @@ -14,6 +14,8 @@ import time from typing import Any, Literal +from psutil._common import POWER_TIME_UNKNOWN, POWER_TIME_UNLIMITED + from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -23,6 +25,7 @@ ) from homeassistant.const import ( PERCENTAGE, + REVOLUTIONS_PER_MINUTE, EntityCategory, UnitOfDataRate, UnitOfInformation, @@ -34,7 +37,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import slugify +from homeassistant.util import dt as dt_util, slugify from . import SystemMonitorConfigEntry from .binary_sensor import BINARY_SENSOR_DOMAIN @@ -55,7 +58,11 @@ SIGNAL_SYSTEMMONITOR_UPDATE = "systemmonitor_update" +BATTERY_REMAIN_UNKNOWNS = (POWER_TIME_UNKNOWN, POWER_TIME_UNLIMITED) + SENSORS_NO_ARG = ( + "battery_empty", + "battery", "last_boot", "load_", "memory_", @@ -64,6 +71,7 @@ ) SENSORS_WITH_ARG = { "disk_": "disk_arguments", + "fan_speed": "fan_speed_arguments", "ipv": "network_arguments", "process_num_fds": "processes", **dict.fromkeys(NET_IO_TYPES, "network_arguments"), @@ -139,6 +147,17 @@ def get_process_num_fds(entity: SystemMonitorSensor) -> int | None: return process_fds.get(entity.argument) +def battery_time_ends(entity: SystemMonitorSensor) -> datetime | None: + """Return when battery runs out, rounded to minute.""" + battery = entity.coordinator.data.battery + if not battery or battery.secsleft in BATTERY_REMAIN_UNKNOWNS: + return None + + return (dt_util.utcnow() + timedelta(seconds=battery.secsleft)).replace( + second=0, microsecond=0 + ) + + @dataclass(frozen=True, kw_only=True) class SysMonitorSensorEntityDescription(SensorEntityDescription): """Describes System Monitor sensor entities.""" @@ -151,6 +170,28 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { + "battery": SysMonitorSensorEntityDescription( + key="battery", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + value_fn=( + lambda entity: entity.coordinator.data.battery.percent + if entity.coordinator.data.battery + else None + ), + none_is_unavailable=True, + add_to_update=lambda entity: ("battery", ""), + ), + "battery_empty": SysMonitorSensorEntityDescription( + key="battery_empty", + translation_key="battery_empty", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.MEASUREMENT, + value_fn=battery_time_ends, + none_is_unavailable=True, + add_to_update=lambda entity: ("battery", ""), + ), "disk_free": SysMonitorSensorEntityDescription( key="disk_free", translation_key="disk_free", @@ -199,6 +240,16 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription): none_is_unavailable=True, add_to_update=lambda entity: ("disks", entity.argument), ), + "fan_speed": SysMonitorSensorEntityDescription( + key="fan_speed", + translation_key="fan_speed", + placeholder="fan_name", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda entity: entity.coordinator.data.fan_speed[entity.argument], + none_is_unavailable=True, + add_to_update=lambda entity: ("fan_speed", ""), + ), "ipv4_address": SysMonitorSensorEntityDescription( key="ipv4_address", translation_key="ipv4_address", @@ -252,8 +303,8 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription): native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round( - entity.coordinator.data.memory.available / 1024**2, 1 + value_fn=( + lambda entity: round(entity.coordinator.data.memory.available / 1024**2, 1) ), add_to_update=lambda entity: ("memory", ""), ), @@ -454,6 +505,7 @@ def get_arguments() -> dict[str, Any]: return { "disk_arguments": get_all_disk_mounts(hass, psutil_wrapper), "network_arguments": get_all_network_interfaces(hass, psutil_wrapper), + "fan_speed_arguments": list(sensor_data.fan_speed), } cpu_temperature: float | None = None diff --git a/homeassistant/components/systemmonitor/strings.json b/homeassistant/components/systemmonitor/strings.json index 861f23fbba2fc..38d0b63dc5106 100644 --- a/homeassistant/components/systemmonitor/strings.json +++ b/homeassistant/components/systemmonitor/strings.json @@ -16,6 +16,9 @@ } }, "sensor": { + "battery_empty": { + "name": "Battery empty" + }, "disk_free": { "name": "Disk free {mount_point}" }, @@ -25,6 +28,9 @@ "disk_use_percent": { "name": "Disk usage {mount_point}" }, + "fan_speed": { + "name": "{fan_name} fan speed" + }, "ipv4_address": { "name": "IPv4 address {ip_address}" }, diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index dec0508bb646f..1118445dab122 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -3,7 +3,7 @@ import logging import os -from psutil._common import shwtemp +from psutil._common import sfan, shwtemp import psutil_home_assistant as ha_psutil from homeassistant.core import HomeAssistant @@ -89,3 +89,19 @@ def read_cpu_temperature(temps: dict[str, list[shwtemp]]) -> float | None: return round(entry.current, 1) return None + + +def read_fan_speed(fans: dict[str, list[sfan]]) -> dict[str, int]: + """Attempt to read fan speed.""" + entry: sfan + + _LOGGER.debug("Fan speed: %s", fans) + if not fans: + return {} + sensor_fans: dict[str, int] = {} + for name, entries in fans.items(): + for entry in entries: + _label = name if not entry.label else entry.label + sensor_fans[_label] = round(entry.current, 0) + + return sensor_fans diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index 0f3a9f7fd1c9c..4ae83b066ba34 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -21,7 +21,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType from .entity import TuyaEntity -from .models import EnumTypeData +from .models import EnumTypeData, find_dpcode from .util import get_dpcode @@ -118,8 +118,8 @@ def __init__( self._attr_unique_id = f"{super().unique_id}{description.key}" # Determine supported modes - if supported_modes := self.find_dpcode( - description.key, dptype=DPType.ENUM, prefer_function=True + if supported_modes := find_dpcode( + self.device, description.key, dptype=DPType.ENUM, prefer_function=True ): if Mode.HOME in supported_modes.range: self._attr_supported_features |= AlarmControlPanelEntityFeature.ARM_HOME @@ -131,8 +131,11 @@ def __init__( self._attr_supported_features |= AlarmControlPanelEntityFeature.TRIGGER # Determine master state - if enum_type := self.find_dpcode( - description.master_state, dptype=DPType.ENUM, prefer_function=True + if enum_type := find_dpcode( + self.device, + description.master_state, + dptype=DPType.ENUM, + prefer_function=True, ): self._master_state = enum_type diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index fe11d2668ef88..fc64dbb7cb26a 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -26,7 +26,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType from .entity import TuyaEntity -from .models import IntegerTypeData +from .models import IntegerTypeData, find_dpcode from .util import get_dpcode TUYA_HVAC_TO_HA = { @@ -153,11 +153,13 @@ def __init__( self._attr_temperature_unit = system_temperature_unit # Figure out current temperature, use preferred unit or what is available - celsius_type = self.find_dpcode( - (DPCode.TEMP_CURRENT, DPCode.UPPER_TEMP), dptype=DPType.INTEGER + celsius_type = find_dpcode( + self.device, (DPCode.TEMP_CURRENT, DPCode.UPPER_TEMP), dptype=DPType.INTEGER ) - fahrenheit_type = self.find_dpcode( - (DPCode.TEMP_CURRENT_F, DPCode.UPPER_TEMP_F), dptype=DPType.INTEGER + fahrenheit_type = find_dpcode( + self.device, + (DPCode.TEMP_CURRENT_F, DPCode.UPPER_TEMP_F), + dptype=DPType.INTEGER, ) if fahrenheit_type and ( prefered_temperature_unit == UnitOfTemperature.FAHRENHEIT @@ -173,11 +175,11 @@ def __init__( self._current_temperature = celsius_type # Figure out setting temperature, use preferred unit or what is available - celsius_type = self.find_dpcode( - DPCode.TEMP_SET, dptype=DPType.INTEGER, prefer_function=True + celsius_type = find_dpcode( + self.device, DPCode.TEMP_SET, dptype=DPType.INTEGER, prefer_function=True ) - fahrenheit_type = self.find_dpcode( - DPCode.TEMP_SET_F, dptype=DPType.INTEGER, prefer_function=True + fahrenheit_type = find_dpcode( + self.device, DPCode.TEMP_SET_F, dptype=DPType.INTEGER, prefer_function=True ) if fahrenheit_type and ( prefered_temperature_unit == UnitOfTemperature.FAHRENHEIT @@ -201,8 +203,8 @@ def __init__( # Determine HVAC modes self._attr_hvac_modes: list[HVACMode] = [] self._hvac_to_tuya = {} - if enum_type := self.find_dpcode( - DPCode.MODE, dptype=DPType.ENUM, prefer_function=True + if enum_type := find_dpcode( + self.device, DPCode.MODE, dptype=DPType.ENUM, prefer_function=True ): self._attr_hvac_modes = [HVACMode.OFF] unknown_hvac_modes: list[str] = [] @@ -225,8 +227,11 @@ def __init__( ] # Determine dpcode to use for setting the humidity - if int_type := self.find_dpcode( - DPCode.HUMIDITY_SET, dptype=DPType.INTEGER, prefer_function=True + if int_type := find_dpcode( + self.device, + DPCode.HUMIDITY_SET, + dptype=DPType.INTEGER, + prefer_function=True, ): self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY self._set_humidity = int_type @@ -234,13 +239,14 @@ def __init__( self._attr_max_humidity = int(int_type.max_scaled) # Determine dpcode to use for getting the current humidity - self._current_humidity = self.find_dpcode( - DPCode.HUMIDITY_CURRENT, dptype=DPType.INTEGER + self._current_humidity = find_dpcode( + self.device, DPCode.HUMIDITY_CURRENT, dptype=DPType.INTEGER ) # Determine fan modes self._fan_mode_dp_code: str | None = None - if enum_type := self.find_dpcode( + if enum_type := find_dpcode( + self.device, (DPCode.FAN_SPEED_ENUM, DPCode.LEVEL, DPCode.WINDSPEED), dptype=DPType.ENUM, prefer_function=True, diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index ac14f7686d07c..4ae6c6dcc807c 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -22,7 +22,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType from .entity import TuyaEntity -from .models import EnumTypeData, IntegerTypeData +from .models import EnumTypeData, IntegerTypeData, find_dpcode from .util import get_dpcode @@ -204,8 +204,8 @@ def __init__( self._attr_supported_features |= ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE ) - elif enum_type := self.find_dpcode( - description.key, dptype=DPType.ENUM, prefer_function=True + elif enum_type := find_dpcode( + self.device, description.key, dptype=DPType.ENUM, prefer_function=True ): if description.open_instruction_value in enum_type.range: self._attr_supported_features |= CoverEntityFeature.OPEN @@ -217,8 +217,11 @@ def __init__( self._current_state = get_dpcode(self.device, description.current_state) # Determine type to use for setting the position - if int_type := self.find_dpcode( - description.set_position, dptype=DPType.INTEGER, prefer_function=True + if int_type := find_dpcode( + self.device, + description.set_position, + dptype=DPType.INTEGER, + prefer_function=True, ): self._attr_supported_features |= CoverEntityFeature.SET_POSITION self._set_position = int_type @@ -226,13 +229,17 @@ def __init__( self._current_position = int_type # Determine type for getting the position - if int_type := self.find_dpcode( - description.current_position, dptype=DPType.INTEGER, prefer_function=True + if int_type := find_dpcode( + self.device, + description.current_position, + dptype=DPType.INTEGER, + prefer_function=True, ): self._current_position = int_type # Determine type to use for setting the tilt - if int_type := self.find_dpcode( + if int_type := find_dpcode( + self.device, (DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL), dptype=DPType.INTEGER, prefer_function=True, @@ -242,7 +249,8 @@ def __init__( # Determine type to use for checking motor reverse mode if (motor_mode := description.motor_reverse_mode) and ( - enum_type := self.find_dpcode( + enum_type := find_dpcode( + self.device, motor_mode, dptype=DPType.ENUM, prefer_function=True, @@ -311,8 +319,11 @@ def is_closed(self) -> bool | None: def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" value: bool | str = True - if self.find_dpcode( - self.entity_description.key, dptype=DPType.ENUM, prefer_function=True + if find_dpcode( + self.device, + self.entity_description.key, + dptype=DPType.ENUM, + prefer_function=True, ): value = self.entity_description.open_instruction_value @@ -337,8 +348,11 @@ def open_cover(self, **kwargs: Any) -> None: def close_cover(self, **kwargs: Any) -> None: """Close cover.""" value: bool | str = False - if self.find_dpcode( - self.entity_description.key, dptype=DPType.ENUM, prefer_function=True + if find_dpcode( + self.device, + self.entity_description.key, + dptype=DPType.ENUM, + prefer_function=True, ): value = self.entity_description.close_instruction_value diff --git a/homeassistant/components/tuya/entity.py b/homeassistant/components/tuya/entity.py index 1de4f8841df0c..ad62af6711484 100644 --- a/homeassistant/components/tuya/entity.py +++ b/homeassistant/components/tuya/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Literal, overload +from typing import Any from tuya_sharing import CustomerDevice, Manager @@ -10,8 +10,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY, DPCode, DPType -from .models import EnumTypeData, IntegerTypeData +from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY class TuyaEntity(Entity): @@ -44,77 +43,6 @@ def available(self) -> bool: """Return if the device is available.""" return self.device.online - @overload - def find_dpcode( - self, - dpcodes: str | DPCode | tuple[DPCode, ...] | None, - *, - prefer_function: bool = False, - dptype: Literal[DPType.ENUM], - ) -> EnumTypeData | None: ... - - @overload - def find_dpcode( - self, - dpcodes: str | DPCode | tuple[DPCode, ...] | None, - *, - prefer_function: bool = False, - dptype: Literal[DPType.INTEGER], - ) -> IntegerTypeData | None: ... - - def find_dpcode( - self, - dpcodes: str | DPCode | tuple[DPCode, ...] | None, - *, - prefer_function: bool = False, - dptype: DPType, - ) -> EnumTypeData | IntegerTypeData | None: - """Find type information for a matching DP code available for this device.""" - if dptype not in (DPType.ENUM, DPType.INTEGER): - raise NotImplementedError("Only ENUM and INTEGER types are supported") - - if dpcodes is None: - return None - - if isinstance(dpcodes, str): - dpcodes = (DPCode(dpcodes),) - elif not isinstance(dpcodes, tuple): - dpcodes = (dpcodes,) - - order = ["status_range", "function"] - if prefer_function: - order = ["function", "status_range"] - - for dpcode in dpcodes: - for key in order: - if dpcode not in getattr(self.device, key): - continue - if ( - dptype == DPType.ENUM - and getattr(self.device, key)[dpcode].type == DPType.ENUM - ): - if not ( - enum_type := EnumTypeData.from_json( - dpcode, getattr(self.device, key)[dpcode].values - ) - ): - continue - return enum_type - - if ( - dptype == DPType.INTEGER - and getattr(self.device, key)[dpcode].type == DPType.INTEGER - ): - if not ( - integer_type := IntegerTypeData.from_json( - dpcode, getattr(self.device, key)[dpcode].values - ) - ): - continue - return integer_type - - return None - async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self.async_on_remove( diff --git a/homeassistant/components/tuya/event.py b/homeassistant/components/tuya/event.py index 4cfb22e4cce4f..20e3c7a9b69a3 100644 --- a/homeassistant/components/tuya/event.py +++ b/homeassistant/components/tuya/event.py @@ -16,6 +16,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType from .entity import TuyaEntity +from .models import find_dpcode # All descriptions can be found here. Mostly the Enum data types in the # default status set of each category (that don't have a set instruction) @@ -125,7 +126,7 @@ def __init__( self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" - if dpcode := self.find_dpcode(description.key, dptype=DPType.ENUM): + if dpcode := find_dpcode(self.device, description.key, dptype=DPType.ENUM): self._attr_event_types: list[str] = dpcode.range async def _handle_state_update( diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index db16720ddc423..7f5bef640d1ea 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -23,7 +23,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType from .entity import TuyaEntity -from .models import EnumTypeData, IntegerTypeData +from .models import EnumTypeData, IntegerTypeData, find_dpcode from .util import get_dpcode _DIRECTION_DPCODES = (DPCode.FAN_DIRECTION,) @@ -106,21 +106,24 @@ def __init__( self._switch = get_dpcode(self.device, _SWITCH_DPCODES) self._attr_preset_modes = [] - if enum_type := self.find_dpcode( - (DPCode.FAN_MODE, DPCode.MODE), dptype=DPType.ENUM, prefer_function=True + if enum_type := find_dpcode( + self.device, + (DPCode.FAN_MODE, DPCode.MODE), + dptype=DPType.ENUM, + prefer_function=True, ): self._presets = enum_type self._attr_supported_features |= FanEntityFeature.PRESET_MODE self._attr_preset_modes = enum_type.range # Find speed controls, can be either percentage or a set of speeds - if int_type := self.find_dpcode( - _SPEED_DPCODES, dptype=DPType.INTEGER, prefer_function=True + if int_type := find_dpcode( + self.device, _SPEED_DPCODES, dptype=DPType.INTEGER, prefer_function=True ): self._attr_supported_features |= FanEntityFeature.SET_SPEED self._speed = int_type - elif enum_type := self.find_dpcode( - _SPEED_DPCODES, dptype=DPType.ENUM, prefer_function=True + elif enum_type := find_dpcode( + self.device, _SPEED_DPCODES, dptype=DPType.ENUM, prefer_function=True ): self._attr_supported_features |= FanEntityFeature.SET_SPEED self._speeds = enum_type @@ -129,8 +132,8 @@ def __init__( self._oscillate = dpcode self._attr_supported_features |= FanEntityFeature.OSCILLATE - if enum_type := self.find_dpcode( - _DIRECTION_DPCODES, dptype=DPType.ENUM, prefer_function=True + if enum_type := find_dpcode( + self.device, _DIRECTION_DPCODES, dptype=DPType.ENUM, prefer_function=True ): self._direction = enum_type self._attr_supported_features |= FanEntityFeature.DIRECTION diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index cc6fdd778fe80..0996560d0f145 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -20,7 +20,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType from .entity import TuyaEntity -from .models import IntegerTypeData +from .models import IntegerTypeData, find_dpcode from .util import ActionDPCodeNotFoundError, get_dpcode @@ -120,23 +120,27 @@ def __init__( ) # Determine humidity parameters - if int_type := self.find_dpcode( - description.humidity, dptype=DPType.INTEGER, prefer_function=True + if int_type := find_dpcode( + self.device, + description.humidity, + dptype=DPType.INTEGER, + prefer_function=True, ): self._set_humidity = int_type self._attr_min_humidity = int(int_type.min_scaled) self._attr_max_humidity = int(int_type.max_scaled) # Determine current humidity DPCode - if int_type := self.find_dpcode( + if int_type := find_dpcode( + self.device, description.current_humidity, dptype=DPType.INTEGER, ): self._current_humidity = int_type # Determine mode support and provided modes - if enum_type := self.find_dpcode( - DPCode.MODE, dptype=DPType.ENUM, prefer_function=True + if enum_type := find_dpcode( + self.device, DPCode.MODE, dptype=DPType.ENUM, prefer_function=True ): self._attr_supported_features |= HumidifierEntityFeature.MODES self._attr_available_modes = enum_type.range diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index ebd13be689ae0..83eea2f41f514 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -28,7 +28,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType, WorkMode from .entity import TuyaEntity -from .models import IntegerTypeData +from .models import IntegerTypeData, find_dpcode from .util import get_dpcode, get_dptype, remap_value @@ -466,16 +466,19 @@ def __init__( # Determine DPCodes self._color_mode_dpcode = get_dpcode(self.device, description.color_mode) - if int_type := self.find_dpcode( - description.brightness, dptype=DPType.INTEGER, prefer_function=True + if int_type := find_dpcode( + self.device, + description.brightness, + dptype=DPType.INTEGER, + prefer_function=True, ): self._brightness = int_type color_modes.add(ColorMode.BRIGHTNESS) - self._brightness_max = self.find_dpcode( - description.brightness_max, dptype=DPType.INTEGER + self._brightness_max = find_dpcode( + self.device, description.brightness_max, dptype=DPType.INTEGER ) - self._brightness_min = self.find_dpcode( - description.brightness_min, dptype=DPType.INTEGER + self._brightness_min = find_dpcode( + self.device, description.brightness_min, dptype=DPType.INTEGER ) if (dpcode := get_dpcode(self.device, description.color_data)) and ( @@ -504,8 +507,11 @@ def __init__( self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2 # Check if the light has color temperature - if int_type := self.find_dpcode( - description.color_temp, dptype=DPType.INTEGER, prefer_function=True + if int_type := find_dpcode( + self.device, + description.color_temp, + dptype=DPType.INTEGER, + prefer_function=True, ): self._color_temp = int_type color_modes.add(ColorMode.COLOR_TEMP) @@ -514,8 +520,11 @@ def __init__( elif ( color_supported(color_modes) and ( - color_mode_enum := self.find_dpcode( - description.color_mode, dptype=DPType.ENUM, prefer_function=True + color_mode_enum := find_dpcode( + self.device, + description.color_mode, + dptype=DPType.ENUM, + prefer_function=True, ) ) and WorkMode.WHITE.value in color_mode_enum.range diff --git a/homeassistant/components/tuya/models.py b/homeassistant/components/tuya/models.py index 82cf5ebd200a4..82df8f45ba50a 100644 --- a/homeassistant/components/tuya/models.py +++ b/homeassistant/components/tuya/models.py @@ -6,12 +6,86 @@ from dataclasses import dataclass import json import struct -from typing import Self +from typing import Literal, Self, overload -from .const import DPCode +from tuya_sharing import CustomerDevice + +from .const import DPCode, DPType from .util import remap_value +@overload +def find_dpcode( + device: CustomerDevice, + dpcodes: str | DPCode | tuple[DPCode, ...] | None, + *, + prefer_function: bool = False, + dptype: Literal[DPType.ENUM], +) -> EnumTypeData | None: ... + + +@overload +def find_dpcode( + device: CustomerDevice, + dpcodes: str | DPCode | tuple[DPCode, ...] | None, + *, + prefer_function: bool = False, + dptype: Literal[DPType.INTEGER], +) -> IntegerTypeData | None: ... + + +def find_dpcode( + device: CustomerDevice, + dpcodes: str | DPCode | tuple[DPCode, ...] | None, + *, + prefer_function: bool = False, + dptype: DPType, +) -> EnumTypeData | IntegerTypeData | None: + """Find type information for a matching DP code available for this device.""" + if dptype not in (DPType.ENUM, DPType.INTEGER): + raise NotImplementedError("Only ENUM and INTEGER types are supported") + + if dpcodes is None: + return None + + if isinstance(dpcodes, str): + dpcodes = (DPCode(dpcodes),) + elif not isinstance(dpcodes, tuple): + dpcodes = (dpcodes,) + + lookup_tuple = ( + (device.function, device.status_range) + if prefer_function + else (device.status_range, device.function) + ) + + for dpcode in dpcodes: + for device_specs in lookup_tuple: + if not ( + (current_definition := device_specs.get(dpcode)) + and current_definition.type == dptype + ): + continue + if dptype is DPType.ENUM: + if not ( + enum_type := EnumTypeData.from_json( + dpcode, current_definition.values + ) + ): + continue + return enum_type + if dptype is DPType.INTEGER: + if not ( + integer_type := IntegerTypeData.from_json( + dpcode, current_definition.values + ) + ): + continue + return integer_type + + return None + + @dataclass class IntegerTypeData: """Integer Type Data.""" diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 1fb00a4de5144..24411899d7df1 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -26,7 +26,7 @@ DPType, ) from .entity import TuyaEntity -from .models import IntegerTypeData +from .models import IntegerTypeData, find_dpcode from .util import ActionDPCodeNotFoundError NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = { @@ -486,8 +486,8 @@ def __init__( self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" - if int_type := self.find_dpcode( - description.key, dptype=DPType.INTEGER, prefer_function=True + if int_type := find_dpcode( + self.device, description.key, dptype=DPType.INTEGER, prefer_function=True ): self._number = int_type self._attr_native_max_value = self._number.max_scaled diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 6a4d8d7b48834..46774f06fdd10 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -13,6 +13,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType from .entity import TuyaEntity +from .models import find_dpcode # All descriptions can be found here. Mostly the Enum data types in the # default instructions set of each category end up being a select. @@ -388,8 +389,8 @@ def __init__( self._attr_unique_id = f"{super().unique_id}{description.key}" self._attr_options: list[str] = [] - if enum_type := self.find_dpcode( - description.key, dptype=DPType.ENUM, prefer_function=True + if enum_type := find_dpcode( + self.device, description.key, dptype=DPType.ENUM, prefer_function=True ): self._attr_options = enum_type.range diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index cd50052d64614..8905bd8449950 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -41,7 +41,13 @@ DPType, ) from .entity import TuyaEntity -from .models import ComplexValue, ElectricityValue, EnumTypeData, IntegerTypeData +from .models import ( + ComplexValue, + ElectricityValue, + EnumTypeData, + IntegerTypeData, + find_dpcode, +) from .util import get_dptype _WIND_DIRECTIONS = { @@ -1684,13 +1690,13 @@ def __init__( f"{super().unique_id}{description.key}{description.subkey or ''}" ) - if int_type := self.find_dpcode(description.key, dptype=DPType.INTEGER): + if int_type := find_dpcode(self.device, description.key, dptype=DPType.INTEGER): self._type_data = int_type self._type = DPType.INTEGER if description.native_unit_of_measurement is None: self._attr_native_unit_of_measurement = int_type.unit - elif enum_type := self.find_dpcode( - description.key, dptype=DPType.ENUM, prefer_function=True + elif enum_type := find_dpcode( + self.device, description.key, dptype=DPType.ENUM, prefer_function=True ): self._type_data = enum_type self._type = DPType.ENUM diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 8e0674ad23a94..f519ec51fb020 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -18,7 +18,7 @@ from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType from .entity import TuyaEntity -from .models import EnumTypeData +from .models import EnumTypeData, find_dpcode from .util import get_dpcode TUYA_MODE_RETURN_HOME = "chargego" @@ -97,8 +97,8 @@ def __init__(self, device: CustomerDevice, device_manager: Manager) -> None: self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME self._return_home_use_switch_charge = True elif ( - enum_type := self.find_dpcode( - DPCode.MODE, dptype=DPType.ENUM, prefer_function=True + enum_type := find_dpcode( + self.device, DPCode.MODE, dptype=DPType.ENUM, prefer_function=True ) ) and TUYA_MODE_RETURN_HOME in enum_type.range: self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME @@ -111,8 +111,8 @@ def __init__(self, device: CustomerDevice, device_manager: Manager) -> None: VacuumEntityFeature.STOP | VacuumEntityFeature.START ) - if enum_type := self.find_dpcode( - DPCode.SUCTION, dptype=DPType.ENUM, prefer_function=True + if enum_type := find_dpcode( + self.device, DPCode.SUCTION, dptype=DPType.ENUM, prefer_function=True ): self._fan_speed = enum_type self._attr_fan_speed_list = enum_type.range diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index ff34134b3fba7..b1556d62d8d57 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -218,6 +218,7 @@ async def test_async_step_integration_discovery( assert result["result"].unique_id == "C666666" +@pytest.mark.usefixtures("mock_ezviz_client") async def test_options_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index e3c2397de7703..7c44f6c9bdecd 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -103,6 +103,7 @@ async def test_form_create_entry_with_auth( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("mock_setup_entry") async def test_reauth_successful(hass: HomeAssistant) -> None: """Test starting a reauthentication flow.""" entry = MockConfigEntry( @@ -375,6 +376,7 @@ async def test_zeroconf_errors(hass: HomeAssistant, error) -> None: assert result["reason"] == reason +@pytest.mark.usefixtures("mock_setup_entry") async def test_reconfigure_successful(hass: HomeAssistant) -> None: """Test starting a reconfigure flow.""" entry = MockConfigEntry( @@ -412,6 +414,7 @@ async def test_reconfigure_successful(hass: HomeAssistant) -> None: } +@pytest.mark.usefixtures("mock_setup_entry") async def test_reconfigure_not_successful(hass: HomeAssistant) -> None: """Test starting a reconfigure flow but no connection found.""" entry = MockConfigEntry( diff --git a/tests/components/systemmonitor/conftest.py b/tests/components/systemmonitor/conftest.py index c44eed77c26a8..0dad667058738 100644 --- a/tests/components/systemmonitor/conftest.py +++ b/tests/components/systemmonitor/conftest.py @@ -7,7 +7,16 @@ from unittest.mock import AsyncMock, Mock, NonCallableMock, patch from psutil import NoSuchProcess, Process -from psutil._common import sdiskpart, sdiskusage, shwtemp, snetio, snicaddr, sswap +from psutil._common import ( + sbattery, + sdiskpart, + sdiskusage, + sfan, + shwtemp, + snetio, + snicaddr, + sswap, +) import pytest from homeassistant.components.systemmonitor.const import DOMAIN @@ -208,6 +217,12 @@ def mock_psutil(mock_process: list[MockProcess]) -> Generator: ] mock_psutil.boot_time.return_value = 1708786800.0 mock_psutil.NoSuchProcess = NoSuchProcess + mock_psutil.sensors_fans.return_value = { + "asus": [sfan("cpu-fan", 1200), sfan("another-fan", 1300)], + } + mock_psutil.sensors_battery.return_value = sbattery( + percent=93, secsleft=16628, power_plugged=False + ) yield mock_psutil diff --git a/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr b/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr index 0c04cfcfa06d6..2c914c53afb91 100644 --- a/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr +++ b/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr @@ -1,4 +1,13 @@ # serializer version: 1 +# name: test_binary_sensor[System Monitor Charging - attributes] + ReadOnlyDict({ + 'device_class': 'battery_charging', + 'friendly_name': 'System Monitor Charging', + }) +# --- +# name: test_binary_sensor[System Monitor Charging - state] + 'off' +# --- # name: test_binary_sensor[System Monitor Process pip - attributes] ReadOnlyDict({ 'device_class': 'running', diff --git a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr index d306fa6551400..97d806614e1fe 100644 --- a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr +++ b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr @@ -8,6 +8,7 @@ 'eth1': "[snicaddr(family=, address='192.168.10.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)]", 'vethxyzxyz': "[snicaddr(family=, address='172.16.10.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)]", }), + 'battery': 'sbattery(percent=93, secsleft=16628, power_plugged=False)', 'boot_time': '2024-02-24 15:00:00+00:00', 'cpu_percent': '10.0', 'disk_usage': dict({ @@ -15,6 +16,10 @@ '/home/notexist/': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', '/media/share': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', }), + 'fan_speed': dict({ + 'another-fan': '1300', + 'cpu-fan': '1200', + }), 'io_counters': dict({ 'eth0': 'snetio(bytes_sent=104857600, bytes_recv=104857600, packets_sent=50, packets_recv=50, errin=0, errout=0, dropin=0, dropout=0)', 'eth1': 'snetio(bytes_sent=209715200, bytes_recv=209715200, packets_sent=150, packets_recv=150, errin=0, errout=0, dropin=0, dropout=0)', @@ -73,6 +78,7 @@ 'coordinators': dict({ 'data': dict({ 'addresses': None, + 'battery': 'sbattery(percent=93, secsleft=16628, power_plugged=False)', 'boot_time': '2024-02-24 15:00:00+00:00', 'cpu_percent': '10.0', 'disk_usage': dict({ @@ -80,6 +86,10 @@ '/home/notexist/': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', '/media/share': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', }), + 'fan_speed': dict({ + 'another-fan': '1300', + 'cpu-fan': '1200', + }), 'io_counters': None, 'load': '(1, 2, 3)', 'memory': 'VirtualMemory(total=104857600, available=41943040, percent=40.0, used=62914560, free=31457280)', diff --git a/tests/components/systemmonitor/snapshots/test_sensor.ambr b/tests/components/systemmonitor/snapshots/test_sensor.ambr index 0ef5375341dbb..9d54f7bfc2af1 100644 --- a/tests/components/systemmonitor/snapshots/test_sensor.ambr +++ b/tests/components/systemmonitor/snapshots/test_sensor.ambr @@ -1,4 +1,25 @@ # serializer version: 1 +# name: test_sensor[System Monitor Battery - attributes] + ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'System Monitor Battery', + 'state_class': , + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[System Monitor Battery - state] + '93' +# --- +# name: test_sensor[System Monitor Battery empty - attributes] + ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'System Monitor Battery empty', + 'state_class': , + }) +# --- +# name: test_sensor[System Monitor Battery empty - state] + '2024-02-24T19:37:00+00:00' +# --- # name: test_sensor[System Monitor Disk free / - attributes] ReadOnlyDict({ 'device_class': 'data_size', @@ -372,3 +393,23 @@ # name: test_sensor[System Monitor Swap use - state] '60.0' # --- +# name: test_sensor[System Monitor another-fan fan speed - attributes] + ReadOnlyDict({ + 'friendly_name': 'System Monitor another-fan fan speed', + 'state_class': , + 'unit_of_measurement': 'rpm', + }) +# --- +# name: test_sensor[System Monitor another-fan fan speed - state] + '1300' +# --- +# name: test_sensor[System Monitor cpu-fan fan speed - attributes] + ReadOnlyDict({ + 'friendly_name': 'System Monitor cpu-fan fan speed', + 'state_class': , + 'unit_of_measurement': 'rpm', + }) +# --- +# name: test_sensor[System Monitor cpu-fan fan speed - state] + '1200' +# --- diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index e22f8e14d3d00..10b7a853a917f 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -23,6 +23,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.freeze_time("2024-02-24 15:00:00", tz_offset=0) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, @@ -543,7 +544,7 @@ async def test_remove_obsolete_entities( mock_added_config_entry.entry_id ) ) - == 39 + == 44 ) entity_registry.async_update_entity( @@ -584,7 +585,7 @@ async def test_remove_obsolete_entities( mock_added_config_entry.entry_id ) ) - == 40 + == 45 ) assert ( diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index da718a989831c..cdefe1bd175ee 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -93,6 +93,7 @@ async def test_reconfigure(hass: HomeAssistant) -> None: } +@pytest.mark.usefixtures("mock_update") async def test_options(hass: HomeAssistant) -> None: """Test options flow.""" entry = MockConfigEntry(