Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable strict typing for generic_hygrostat #107272

Merged
merged 1 commit into from Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .strict-typing
Expand Up @@ -172,6 +172,7 @@ homeassistant.components.fritzbox_callmonitor.*
homeassistant.components.fronius.*
homeassistant.components.frontend.*
homeassistant.components.fully_kiosk.*
homeassistant.components.generic_hygrostat.*
homeassistant.components.geo_location.*
homeassistant.components.geocaching.*
homeassistant.components.gios.*
Expand Down
151 changes: 84 additions & 67 deletions homeassistant/components/generic_hygrostat/humidifier.py
Expand Up @@ -2,7 +2,10 @@
from __future__ import annotations

import asyncio
from collections.abc import Callable
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING, Any

from homeassistant.components.humidifier import (
ATTR_HUMIDITY,
Expand All @@ -27,7 +30,13 @@
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback
from homeassistant.core import (
DOMAIN as HA_DOMAIN,
Event,
HomeAssistant,
State,
callback,
)
from homeassistant.helpers import condition
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import (
Expand Down Expand Up @@ -72,22 +81,22 @@
"""Set up the generic hygrostat platform."""
if discovery_info:
config = discovery_info
name = config[CONF_NAME]
switch_entity_id = config[CONF_HUMIDIFIER]
sensor_entity_id = config[CONF_SENSOR]
min_humidity = config.get(CONF_MIN_HUMIDITY)
max_humidity = config.get(CONF_MAX_HUMIDITY)
target_humidity = config.get(CONF_TARGET_HUMIDITY)
device_class = config.get(CONF_DEVICE_CLASS)
min_cycle_duration = config.get(CONF_MIN_DUR)
sensor_stale_duration = config.get(CONF_STALE_DURATION)
dry_tolerance = config[CONF_DRY_TOLERANCE]
wet_tolerance = config[CONF_WET_TOLERANCE]
keep_alive = config.get(CONF_KEEP_ALIVE)
initial_state = config.get(CONF_INITIAL_STATE)
away_humidity = config.get(CONF_AWAY_HUMIDITY)
away_fixed = config.get(CONF_AWAY_FIXED)
unique_id = config.get(CONF_UNIQUE_ID)
name: str = config[CONF_NAME]
switch_entity_id: str = config[CONF_HUMIDIFIER]
sensor_entity_id: str = config[CONF_SENSOR]
min_humidity: int | None = config.get(CONF_MIN_HUMIDITY)
max_humidity: int | None = config.get(CONF_MAX_HUMIDITY)
target_humidity: int | None = config.get(CONF_TARGET_HUMIDITY)
device_class: HumidifierDeviceClass | None = config.get(CONF_DEVICE_CLASS)
min_cycle_duration: timedelta | None = config.get(CONF_MIN_DUR)
sensor_stale_duration: timedelta | None = config.get(CONF_STALE_DURATION)
dry_tolerance: float = config[CONF_DRY_TOLERANCE]
wet_tolerance: float = config[CONF_WET_TOLERANCE]
keep_alive: timedelta | None = config.get(CONF_KEEP_ALIVE)
initial_state: bool | None = config.get(CONF_INITIAL_STATE)
away_humidity: int | None = config.get(CONF_AWAY_HUMIDITY)
away_fixed: bool | None = config.get(CONF_AWAY_FIXED)
unique_id: str | None = config.get(CONF_UNIQUE_ID)

async_add_entities(
[
Expand Down Expand Up @@ -120,36 +129,36 @@

def __init__(
self,
name,
switch_entity_id,
sensor_entity_id,
min_humidity,
max_humidity,
target_humidity,
device_class,
min_cycle_duration,
dry_tolerance,
wet_tolerance,
keep_alive,
initial_state,
away_humidity,
away_fixed,
sensor_stale_duration,
unique_id,
):
name: str,
switch_entity_id: str,
sensor_entity_id: str,
min_humidity: int | None,
max_humidity: int | None,
target_humidity: int | None,
device_class: HumidifierDeviceClass | None,
min_cycle_duration: timedelta | None,
dry_tolerance: float,
wet_tolerance: float,
keep_alive: timedelta | None,
initial_state: bool | None,
away_humidity: int | None,
away_fixed: bool | None,
sensor_stale_duration: timedelta | None,
unique_id: str | None,
) -> None:
"""Initialize the hygrostat."""
self._name = name
self._switch_entity_id = switch_entity_id
self._sensor_entity_id = sensor_entity_id
self._device_class = device_class
self._device_class = device_class or HumidifierDeviceClass.HUMIDIFIER
self._min_cycle_duration = min_cycle_duration
self._dry_tolerance = dry_tolerance
self._wet_tolerance = wet_tolerance
self._keep_alive = keep_alive
self._state = initial_state
self._saved_target_humidity = away_humidity or target_humidity
self._active = False
self._cur_humidity = None
self._cur_humidity: float | None = None
self._humidity_lock = asyncio.Lock()
self._min_humidity = min_humidity
self._max_humidity = max_humidity
Expand All @@ -159,14 +168,12 @@
self._away_humidity = away_humidity
self._away_fixed = away_fixed
self._sensor_stale_duration = sensor_stale_duration
self._remove_stale_tracking = None
self._remove_stale_tracking: Callable[[], None] | None = None
self._is_away = False
if not self._device_class:
self._device_class = HumidifierDeviceClass.HUMIDIFIER
self._attr_action = HumidifierAction.IDLE
self._attr_unique_id = unique_id

async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added."""
await super().async_added_to_hass()

Expand All @@ -185,7 +192,7 @@
)
)

async def _async_startup(event):
async def _async_startup(event: Event | None) -> None:
"""Init on startup."""
sensor_state = self.hass.states.get(self._sensor_entity_id)
if sensor_state is None or sensor_state.state in (
Expand Down Expand Up @@ -234,39 +241,39 @@
return await super().async_will_remove_from_hass()

@property
def available(self):
def available(self) -> bool:
"""Return True if entity is available."""
return self._active

@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the optional state attributes."""
if self._saved_target_humidity:
return {ATTR_SAVED_HUMIDITY: self._saved_target_humidity}
return None

@property
def name(self):
def name(self) -> str:
"""Return the name of the hygrostat."""
return self._name

@property
def is_on(self):
def is_on(self) -> bool | None:
"""Return true if the hygrostat is on."""
return self._state

@property
def current_humidity(self):
def current_humidity(self) -> int | None:
"""Return the measured humidity."""
return self._cur_humidity
return int(self._cur_humidity) if self._cur_humidity is not None else None

@property
def target_humidity(self):
def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
return self._target_humidity

@property
def mode(self):
def mode(self) -> str | None:
"""Return the current mode."""
if self._away_humidity is None:
return None
Expand All @@ -275,26 +282,26 @@
return MODE_NORMAL

@property
def available_modes(self):
def available_modes(self) -> list[str] | None:
"""Return a list of available modes."""
if self._away_humidity:
return [MODE_NORMAL, MODE_AWAY]
return None

@property
def device_class(self):
def device_class(self) -> HumidifierDeviceClass:
"""Return the device class of the humidifier."""
return self._device_class

async def async_turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn hygrostat on."""
if not self._active:
return
self._state = True
await self._async_operate(force=True)
self.async_write_ha_state()

async def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn hygrostat off."""
if not self._active:
return
Expand All @@ -306,7 +313,7 @@
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
if humidity is None:
return
return # type: ignore[unreachable]

Check warning on line 316 in homeassistant/components/generic_hygrostat/humidifier.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/generic_hygrostat/humidifier.py#L316

Added line #L316 was not covered by tests

if self._is_away and self._away_fixed:
self._saved_target_humidity = humidity
Expand All @@ -318,7 +325,7 @@
self.async_write_ha_state()

@property
def min_humidity(self):
def min_humidity(self) -> int:
"""Return the minimum humidity."""
if self._min_humidity:
return self._min_humidity
Expand All @@ -327,15 +334,17 @@
return super().min_humidity

@property
def max_humidity(self):
def max_humidity(self) -> int:
"""Return the maximum humidity."""
if self._max_humidity:
return self._max_humidity

# Get default humidity from super class
return super().max_humidity

async def _async_sensor_changed(self, entity_id, old_state, new_state):
async def _async_sensor_changed(
self, entity_id: str, old_state: State | None, new_state: State | None
) -> None:
"""Handle ambient humidity changes."""
if new_state is None:
return
Expand All @@ -353,18 +362,21 @@
await self._async_operate()
self.async_write_ha_state()

async def _async_sensor_not_responding(self, now=None):
async def _async_sensor_not_responding(self, now: datetime | None = None) -> None:
"""Handle sensor stale event."""

state = self.hass.states.get(self._sensor_entity_id)
_LOGGER.debug(
"Sensor has not been updated for %s",
now - self.hass.states.get(self._sensor_entity_id).last_updated,
now - state.last_updated if now and state else "---",
)
_LOGGER.warning("Sensor is stalled, call the emergency stop")
await self._async_update_humidity("Stalled")

@callback
def _async_switch_changed(self, entity_id, old_state, new_state):
def _async_switch_changed(
self, entity_id: str, old_state: State | None, new_state: State | None
) -> None:
"""Handle humidifier switch state changes."""
if new_state is None:
return
Expand All @@ -379,7 +391,7 @@

self.async_schedule_update_ha_state()

async def _async_update_humidity(self, humidity):
async def _async_update_humidity(self, humidity: str) -> None:
"""Update hygrostat with latest state from sensor."""
try:
self._cur_humidity = float(humidity)
Expand All @@ -390,7 +402,9 @@
if self._is_device_active:
await self._async_device_turn_off()

async def _async_operate(self, time=None, force=False):
async def _async_operate(
self, time: datetime | None = None, force: bool = False
) -> None:
"""Check if we need to turn humidifying on or off."""
async with self._humidity_lock:
if not self._active and None not in (
Expand Down Expand Up @@ -432,12 +446,15 @@

if force:
# Ignore the tolerance when switched on manually
dry_tolerance = 0
wet_tolerance = 0
dry_tolerance: float = 0
wet_tolerance: float = 0
else:
dry_tolerance = self._dry_tolerance
wet_tolerance = self._wet_tolerance

if TYPE_CHECKING:
assert self._target_humidity is not None
assert self._cur_humidity is not None
too_dry = self._target_humidity - self._cur_humidity >= dry_tolerance
too_wet = self._cur_humidity - self._target_humidity >= wet_tolerance
if self._is_device_active:
Expand All @@ -461,16 +478,16 @@
await self._async_device_turn_off()

@property
def _is_device_active(self):
def _is_device_active(self) -> bool:
"""If the toggleable device is currently active."""
return self.hass.states.is_state(self._switch_entity_id, STATE_ON)

async def _async_device_turn_on(self):
async def _async_device_turn_on(self) -> None:
"""Turn humidifier toggleable device on."""
data = {ATTR_ENTITY_ID: self._switch_entity_id}
await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data)

async def _async_device_turn_off(self):
async def _async_device_turn_off(self) -> None:
"""Turn humidifier toggleable device off."""
data = {ATTR_ENTITY_ID: self._switch_entity_id}
await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data)
Expand Down
10 changes: 10 additions & 0 deletions mypy.ini
Expand Up @@ -1481,6 +1481,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true

[mypy-homeassistant.components.generic_hygrostat.*]
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.geo_location.*]
check_untyped_defs = true
disallow_incomplete_defs = true
Expand Down