diff --git a/.strict-typing b/.strict-typing index 44e303152ce..559ee64ddd7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -182,6 +182,7 @@ homeassistant.components.media_player.* homeassistant.components.media_source.* homeassistant.components.metoffice.* homeassistant.components.mikrotik.* +homeassistant.components.min_max.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 87edcd38766..c052cbc211d 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -1,8 +1,10 @@ """Support for displaying minimal, maximal, mean or median values.""" from __future__ import annotations +from datetime import datetime import logging import statistics +from typing import Any import voluptuous as vol @@ -20,12 +22,17 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + EventType, + StateType, +) from . import PLATFORMS from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN @@ -62,7 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ENTITY_IDS): cv.entity_ids, vol.Optional(CONF_ROUND_DIGITS, default=2): vol.Coerce(int), - vol.Optional(CONF_UNIQUE_ID): str, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -100,10 +107,10 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the min/max/mean sensor.""" - entity_ids = config.get(CONF_ENTITY_IDS) - name = config.get(CONF_NAME) - sensor_type = config.get(CONF_TYPE) - round_digits = config.get(CONF_ROUND_DIGITS) + entity_ids: list[str] = config[CONF_ENTITY_IDS] + name: str | None = config.get(CONF_NAME) + sensor_type: str = config[CONF_TYPE] + round_digits: int = config[CONF_ROUND_DIGITS] unique_id = config.get(CONF_UNIQUE_ID) await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -113,10 +120,10 @@ async def async_setup_platform( ) -def calc_min(sensor_values): +def calc_min(sensor_values: list[tuple[str, Any]]) -> tuple[str | None, float | None]: """Calculate min value, honoring unknown states.""" - val = None - entity_id = None + val: float | None = None + entity_id: str | None = None for sensor_id, sensor_value in sensor_values: if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and ( val is None or val > sensor_value @@ -125,10 +132,10 @@ def calc_min(sensor_values): return entity_id, val -def calc_max(sensor_values): +def calc_max(sensor_values: list[tuple[str, Any]]) -> tuple[str | None, float | None]: """Calculate max value, honoring unknown states.""" - val = None - entity_id = None + val: float | None = None + entity_id: str | None = None for sensor_id, sensor_value in sensor_values: if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and ( val is None or val < sensor_value @@ -137,7 +144,7 @@ def calc_max(sensor_values): return entity_id, val -def calc_mean(sensor_values, round_digits): +def calc_mean(sensor_values: list[tuple[str, Any]], round_digits: int) -> float | None: """Calculate mean value, honoring unknown states.""" result = [ sensor_value @@ -147,10 +154,13 @@ def calc_mean(sensor_values, round_digits): if not result: return None - return round(statistics.mean(result), round_digits) + value: float = round(statistics.mean(result), round_digits) + return value -def calc_median(sensor_values, round_digits): +def calc_median( + sensor_values: list[tuple[str, Any]], round_digits: int +) -> float | None: """Calculate median value, honoring unknown states.""" result = [ sensor_value @@ -160,10 +170,11 @@ def calc_median(sensor_values, round_digits): if not result: return None - return round(statistics.median(result), round_digits) + value: float = round(statistics.median(result), round_digits) + return value -def calc_range(sensor_values, round_digits): +def calc_range(sensor_values: list[tuple[str, Any]], round_digits: int) -> float | None: """Calculate range value, honoring unknown states.""" result = [ sensor_value @@ -173,7 +184,8 @@ def calc_range(sensor_values, round_digits): if not result: return None - return round(max(result) - min(result), round_digits) + value: float = round(max(result) - min(result), round_digits) + return value class MinMaxSensor(SensorEntity): @@ -183,7 +195,14 @@ class MinMaxSensor(SensorEntity): _attr_should_poll = False _attr_state_class = SensorStateClass.MEASUREMENT - def __init__(self, entity_ids, name, sensor_type, round_digits, unique_id): + def __init__( + self, + entity_ids: list[str], + name: str | None, + sensor_type: str, + round_digits: int, + unique_id: str | None, + ) -> None: """Initialize the min/max sensor.""" self._attr_unique_id = unique_id self._entity_ids = entity_ids @@ -197,11 +216,17 @@ class MinMaxSensor(SensorEntity): self._sensor_attr = SENSOR_TYPE_TO_ATTR[self._sensor_type] self._unit_of_measurement = None self._unit_of_measurement_mismatch = False - self.min_value = self.max_value = self.mean = self.last = self.median = None - self.range = None - self.min_entity_id = self.max_entity_id = self.last_entity_id = None + self.min_value: float | None = None + self.max_value: float | None = None + self.mean: float | None = None + self.last: float | None = None + self.median: float | None = None + self.range: float | None = None + self.min_entity_id: str | None = None + self.max_entity_id: str | None = None + self.last_entity_id: str | None = None self.count_sensors = len(self._entity_ids) - self.states = {} + self.states: dict[str, Any] = {} async def async_added_to_hass(self) -> None: """Handle added to Hass.""" @@ -220,21 +245,22 @@ class MinMaxSensor(SensorEntity): self._calc_values() @property - def native_value(self): + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" if self._unit_of_measurement_mismatch: return None - return getattr(self, self._sensor_attr) + value: StateType | datetime = getattr(self, self._sensor_attr) + return value @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if self._unit_of_measurement_mismatch: return "ERR" return self._unit_of_measurement @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the sensor.""" if self._sensor_type == "min": return {ATTR_MIN_ENTITY_ID: self.min_entity_id} @@ -245,10 +271,12 @@ class MinMaxSensor(SensorEntity): return None @callback - def _async_min_max_sensor_state_listener(self, event, update_state=True): + def _async_min_max_sensor_state_listener( + self, event: EventType, update_state: bool = True + ) -> None: """Handle the sensor state changes.""" - new_state = event.data.get("new_state") - entity = event.data.get("entity_id") + new_state: State | None = event.data.get("new_state") + entity: str = event.data["entity_id"] if ( new_state is None @@ -296,7 +324,7 @@ class MinMaxSensor(SensorEntity): self.async_write_ha_state() @callback - def _calc_values(self): + def _calc_values(self) -> None: """Calculate the values.""" sensor_values = [ (entity_id, self.states[entity_id]) diff --git a/mypy.ini b/mypy.ini index c0628a2fc71..a347218cb70 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1573,6 +1573,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.min_max.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py index 0eb334763d6..2503eefb1b0 100644 --- a/tests/components/min_max/test_config_flow.py +++ b/tests/components/min_max/test_config_flow.py @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize("platform", ("sensor",)) -async def test_config_flow(hass: HomeAssistant, platform) -> None: +async def test_config_flow(hass: HomeAssistant, platform: str) -> None: """Test the config flow.""" input_sensors = ["sensor.input_one", "sensor.input_two"] @@ -66,7 +66,7 @@ def get_suggested(schema, key): @pytest.mark.parametrize("platform", ("sensor",)) -async def test_options(hass: HomeAssistant, platform) -> None: +async def test_options(hass: HomeAssistant, platform: str) -> None: """Test reconfiguring.""" hass.states.async_set("sensor.input_one", "10") hass.states.async_set("sensor.input_two", "20") diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 417bcda2d91..96cbf7317c7 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -35,7 +35,7 @@ RANGE_1_DIGIT = round(max(VALUES) - min(VALUES), 1) RANGE_4_DIGITS = round(max(VALUES) - min(VALUES), 4) -async def test_default_name_sensor(hass): +async def test_default_name_sensor(hass: HomeAssistant) -> None: """Test the min sensor with a default name.""" config = { "sensor": { @@ -60,7 +60,7 @@ async def test_default_name_sensor(hass): assert entity_ids[2] == state.attributes.get("min_entity_id") -async def test_min_sensor(hass): +async def test_min_sensor(hass: HomeAssistant) -> None: """Test the min sensor.""" config = { "sensor": { @@ -92,7 +92,7 @@ async def test_min_sensor(hass): assert entity.unique_id == "very_unique_id" -async def test_max_sensor(hass): +async def test_max_sensor(hass: HomeAssistant) -> None: """Test the max sensor.""" config = { "sensor": { @@ -119,7 +119,7 @@ async def test_max_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT -async def test_mean_sensor(hass): +async def test_mean_sensor(hass: HomeAssistant) -> None: """Test the mean sensor.""" config = { "sensor": { @@ -145,7 +145,7 @@ async def test_mean_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT -async def test_mean_1_digit_sensor(hass): +async def test_mean_1_digit_sensor(hass: HomeAssistant) -> None: """Test the mean with 1-digit precision sensor.""" config = { "sensor": { @@ -171,7 +171,7 @@ async def test_mean_1_digit_sensor(hass): assert str(float(MEAN_1_DIGIT)) == state.state -async def test_mean_4_digit_sensor(hass): +async def test_mean_4_digit_sensor(hass: HomeAssistant) -> None: """Test the mean with 4-digit precision sensor.""" config = { "sensor": { @@ -197,7 +197,7 @@ async def test_mean_4_digit_sensor(hass): assert str(float(MEAN_4_DIGITS)) == state.state -async def test_median_sensor(hass): +async def test_median_sensor(hass: HomeAssistant) -> None: """Test the median sensor.""" config = { "sensor": { @@ -223,7 +223,7 @@ async def test_median_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT -async def test_range_4_digit_sensor(hass): +async def test_range_4_digit_sensor(hass: HomeAssistant) -> None: """Test the range with 4-digit precision sensor.""" config = { "sensor": { @@ -249,7 +249,7 @@ async def test_range_4_digit_sensor(hass): assert str(float(RANGE_4_DIGITS)) == state.state -async def test_range_1_digit_sensor(hass): +async def test_range_1_digit_sensor(hass: HomeAssistant) -> None: """Test the range with 1-digit precision sensor.""" config = { "sensor": { @@ -275,7 +275,7 @@ async def test_range_1_digit_sensor(hass): assert str(float(RANGE_1_DIGIT)) == state.state -async def test_not_enough_sensor_value(hass): +async def test_not_enough_sensor_value(hass: HomeAssistant) -> None: """Test that there is nothing done if not enough values available.""" config = { "sensor": { @@ -327,7 +327,7 @@ async def test_not_enough_sensor_value(hass): assert state.attributes.get("max_value") is None -async def test_different_unit_of_measurement(hass): +async def test_different_unit_of_measurement(hass: HomeAssistant) -> None: """Test for different unit of measurement.""" config = { "sensor": { @@ -374,7 +374,7 @@ async def test_different_unit_of_measurement(hass): assert state.attributes.get("unit_of_measurement") == "ERR" -async def test_last_sensor(hass): +async def test_last_sensor(hass: HomeAssistant) -> None: """Test the last sensor.""" config = { "sensor": { @@ -399,7 +399,7 @@ async def test_last_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT -async def test_reload(hass): +async def test_reload(hass: HomeAssistant) -> None: """Verify we can reload filter sensors.""" hass.states.async_set("sensor.test_1", 12345) hass.states.async_set("sensor.test_2", 45678)