diff --git a/.strict-typing b/.strict-typing index b33eab5bd65..9db95008927 100644 --- a/.strict-typing +++ b/.strict-typing @@ -297,6 +297,7 @@ homeassistant.components.tag.* homeassistant.components.tailscale.* homeassistant.components.tautulli.* homeassistant.components.tcp.* +homeassistant.components.threshold.* homeassistant.components.tibber.* homeassistant.components.tile.* homeassistant.components.tilt_ble.* diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 8cec85bf20d..0badf7eb41f 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -2,12 +2,14 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -19,7 +21,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, 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 @@ -93,12 +95,15 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Threshold sensor.""" - entity_id = config.get(CONF_ENTITY_ID) - name = config.get(CONF_NAME) - lower = config.get(CONF_LOWER) - upper = config.get(CONF_UPPER) - hysteresis = config.get(CONF_HYSTERESIS) - device_class = config.get(CONF_DEVICE_CLASS) + entity_id: str = config[CONF_ENTITY_ID] + name: str = config[CONF_NAME] + lower: float | None = config.get(CONF_LOWER) + upper: float | None = config.get(CONF_UPPER) + hysteresis: float = config[CONF_HYSTERESIS] + device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS) + + if lower is None and upper is None: + raise ValueError("Lower or Upper thresholds not provided") async_add_entities( [ @@ -115,22 +120,29 @@ class ThresholdSensor(BinarySensorEntity): _attr_should_poll = False def __init__( - self, hass, entity_id, name, lower, upper, hysteresis, device_class, unique_id - ): + self, + hass: HomeAssistant, + entity_id: str, + name: str, + lower: float | None, + upper: float | None, + hysteresis: float, + device_class: BinarySensorDeviceClass | None, + unique_id: str | None, + ) -> None: """Initialize the Threshold sensor.""" self._attr_unique_id = unique_id self._entity_id = entity_id self._name = name self._threshold_lower = lower self._threshold_upper = upper - self._hysteresis = hysteresis + self._hysteresis: float = hysteresis self._device_class = device_class - self._state_position = POSITION_UNKNOWN - self._state = None - self.sensor_value = None + self._state: bool | None = None + self.sensor_value: float | None = None - def _update_sensor_state(): + def _update_sensor_state() -> None: """Handle sensor state changes.""" if (new_state := hass.states.get(self._entity_id)) is None: return @@ -148,7 +160,7 @@ class ThresholdSensor(BinarySensorEntity): self._update_state() @callback - def async_threshold_sensor_state_listener(event): + def async_threshold_sensor_state_listener(event: Event) -> None: """Handle sensor state changes.""" _update_sensor_state() self.async_write_ha_state() @@ -161,32 +173,31 @@ class ThresholdSensor(BinarySensorEntity): _update_sensor_state() @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._state @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass | None: """Return the sensor class of the sensor.""" return self._device_class @property - def threshold_type(self): + def threshold_type(self) -> str: """Return the type of threshold this sensor represents.""" if self._threshold_lower is not None and self._threshold_upper is not None: return TYPE_RANGE if self._threshold_lower is not None: return TYPE_LOWER - if self._threshold_upper is not None: - return TYPE_UPPER + return TYPE_UPPER @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" return { ATTR_ENTITY_ID: self._entity_id, @@ -199,44 +210,51 @@ class ThresholdSensor(BinarySensorEntity): } @callback - def _update_state(self): + def _update_state(self) -> None: """Update the state.""" - def below(threshold): + def below(sensor_value: float, threshold: float) -> bool: """Determine if the sensor value is below a threshold.""" - return self.sensor_value < (threshold - self._hysteresis) + return sensor_value < (threshold - self._hysteresis) - def above(threshold): + def above(sensor_value: float, threshold: float) -> bool: """Determine if the sensor value is above a threshold.""" - return self.sensor_value > (threshold + self._hysteresis) + return sensor_value > (threshold + self._hysteresis) if self.sensor_value is None: self._state_position = POSITION_UNKNOWN self._state = False + return - elif self.threshold_type == TYPE_LOWER: - if below(self._threshold_lower): + if self.threshold_type == TYPE_LOWER and self._threshold_lower is not None: + if below(self.sensor_value, self._threshold_lower): self._state_position = POSITION_BELOW self._state = True - elif above(self._threshold_lower): + elif above(self.sensor_value, self._threshold_lower): self._state_position = POSITION_ABOVE self._state = False - elif self.threshold_type == TYPE_UPPER: - if above(self._threshold_upper): + if self.threshold_type == TYPE_UPPER and self._threshold_upper is not None: + if above(self.sensor_value, self._threshold_upper): self._state_position = POSITION_ABOVE self._state = True - elif below(self._threshold_upper): + elif below(self.sensor_value, self._threshold_upper): self._state_position = POSITION_BELOW self._state = False - elif self.threshold_type == TYPE_RANGE: - if below(self._threshold_lower): + if ( + self.threshold_type == TYPE_RANGE + and self._threshold_lower is not None + and self._threshold_upper is not None + ): + if below(self.sensor_value, self._threshold_lower): self._state_position = POSITION_BELOW self._state = False - if above(self._threshold_upper): + if above(self.sensor_value, self._threshold_upper): self._state_position = POSITION_ABOVE self._state = False - elif above(self._threshold_lower) and below(self._threshold_upper): + elif above(self.sensor_value, self._threshold_lower) and below( + self.sensor_value, self._threshold_upper + ): self._state_position = POSITION_IN_RANGE self._state = True diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index fbb12872306..31d51fee3f3 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -76,4 +76,5 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" - return options[CONF_NAME] + name: str = options[CONF_NAME] + return name diff --git a/mypy.ini b/mypy.ini index 6d32c16b96b..760c7f6811d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2733,6 +2733,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.threshold.*] +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.tibber.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index f009e4c48a2..eed3a8a40e0 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -1,4 +1,5 @@ """The test for the threshold sensor platform.""" + import pytest from homeassistant.const import ( @@ -567,3 +568,20 @@ async def test_sensor_upper_zero_threshold(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("binary_sensor.threshold") assert state.state == "on" + + +async def test_sensor_no_lower_upper( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test if no lower or upper has been provided.""" + config = { + "binary_sensor": { + "platform": "threshold", + "entity_id": "sensor.test_monitored", + } + } + + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + assert "Lower or Upper thresholds not provided" in caplog.text