146 lines
5.3 KiB
Python
146 lines
5.3 KiB
Python
"""Fan platform for the Novy Cooker Hood (calibrated speed control)."""
|
|
|
|
import math
|
|
from typing import Any
|
|
|
|
from rf_protocols.codes.novy.cooker_hood import NovyCookerHoodButton
|
|
|
|
from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntity, FanEntityFeature
|
|
from homeassistant.components.radio_frequency import (
|
|
RadioFrequencyTransmitterConsumerEntity,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import CONF_CODE
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.helpers.restore_state import RestoreEntity
|
|
from homeassistant.util.percentage import (
|
|
percentage_to_ranged_value,
|
|
ranged_value_to_percentage,
|
|
)
|
|
|
|
from .const import CONF_TRANSMITTER, SPEED_COUNT
|
|
from .entity import NovyCookerHoodEntity
|
|
|
|
PARALLEL_UPDATES = 1
|
|
|
|
_SPEED_RANGE = (1, SPEED_COUNT)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Novy Cooker Hood fan platform."""
|
|
async_add_entities([NovyCookerHoodFan(config_entry)])
|
|
|
|
|
|
class NovyCookerHoodFan(
|
|
NovyCookerHoodEntity,
|
|
RadioFrequencyTransmitterConsumerEntity,
|
|
FanEntity,
|
|
RestoreEntity,
|
|
):
|
|
"""Calibration-based fan: each change resets to off then climbs to target."""
|
|
|
|
_attr_name = None
|
|
_attr_speed_count = SPEED_COUNT
|
|
_attr_supported_features = (
|
|
FanEntityFeature.TURN_ON
|
|
| FanEntityFeature.TURN_OFF
|
|
| FanEntityFeature.SET_SPEED
|
|
)
|
|
|
|
def __init__(self, entry: ConfigEntry) -> None:
|
|
"""Initialize the fan."""
|
|
super().__init__(entry)
|
|
self._rf_transmitter_entity_id = entry.data[CONF_TRANSMITTER]
|
|
self._code: int = entry.data[CONF_CODE]
|
|
self._level = 0
|
|
self._attr_unique_id = entry.entry_id
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return whether the fan is currently on."""
|
|
return self._level > 0
|
|
|
|
@property
|
|
def percentage(self) -> int:
|
|
"""Return the current speed as a percentage."""
|
|
if self._level == 0:
|
|
return 0
|
|
return ranged_value_to_percentage(_SPEED_RANGE, self._level)
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Restore the last known speed level from the saved percentage."""
|
|
await super().async_added_to_hass()
|
|
last = await self.async_get_last_state()
|
|
if last is None:
|
|
return
|
|
last_pct = last.attributes.get(ATTR_PERCENTAGE)
|
|
if isinstance(last_pct, (int, float)) and last_pct > 0:
|
|
self._level = math.ceil(percentage_to_ranged_value(_SPEED_RANGE, last_pct))
|
|
|
|
async def async_turn_on(
|
|
self,
|
|
percentage: int | None = None,
|
|
preset_mode: str | None = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Turn on at the requested level (default = 1)."""
|
|
if percentage is None or percentage <= 0:
|
|
level = 1
|
|
else:
|
|
level = math.ceil(percentage_to_ranged_value(_SPEED_RANGE, percentage))
|
|
await self._async_set_level(level)
|
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
"""Turn the fan off by sending the calibration sequence to level 0."""
|
|
await self._async_set_level(0)
|
|
|
|
async def async_set_percentage(self, percentage: int) -> None:
|
|
"""Set the fan speed via calibration."""
|
|
if percentage <= 0:
|
|
await self._async_set_level(0)
|
|
return
|
|
level = math.ceil(percentage_to_ranged_value(_SPEED_RANGE, percentage))
|
|
await self._async_set_level(level)
|
|
|
|
async def async_increase_speed(self, percentage_step: int | None = None) -> None:
|
|
"""Bump speed up by N hardware levels (no recalibration)."""
|
|
steps = self._steps_from_percentage(percentage_step)
|
|
plus = NovyCookerHoodButton.PLUS.to_command(channel=self._code)
|
|
for _ in range(steps):
|
|
await self._send_command(plus)
|
|
self._level = min(SPEED_COUNT, self._level + steps)
|
|
self.async_write_ha_state()
|
|
|
|
async def async_decrease_speed(self, percentage_step: int | None = None) -> None:
|
|
"""Bump speed down by N hardware levels (no recalibration)."""
|
|
steps = self._steps_from_percentage(percentage_step)
|
|
minus = NovyCookerHoodButton.MINUS.to_command(channel=self._code)
|
|
for _ in range(steps):
|
|
await self._send_command(minus)
|
|
self._level = max(0, self._level - steps)
|
|
self.async_write_ha_state()
|
|
|
|
@staticmethod
|
|
def _steps_from_percentage(percentage_step: int | None) -> int:
|
|
"""Convert a percentage step into a number of hardware level presses."""
|
|
if percentage_step is None:
|
|
return 1
|
|
return math.ceil(percentage_step * SPEED_COUNT / 100)
|
|
|
|
async def _async_set_level(self, level: int) -> None:
|
|
"""Reset to off with `SPEED_COUNT` minus presses, then climb to level."""
|
|
minus = NovyCookerHoodButton.MINUS.to_command(channel=self._code)
|
|
for _ in range(SPEED_COUNT):
|
|
await self._send_command(minus)
|
|
if level > 0:
|
|
plus = NovyCookerHoodButton.PLUS.to_command(channel=self._code)
|
|
for _ in range(level):
|
|
await self._send_command(plus)
|
|
self._level = level
|
|
self.async_write_ha_state()
|