Compare commits

...

7 Commits

Author SHA1 Message Date
Paul Bottein d388a0219f Fix tests 2026-05-26 18:16:15 +02:00
Paul Bottein b94fa8e0ad Adjust delay using real device 2026-05-26 18:14:10 +02:00
Paul Bottein 33a6bc6063 improve test 2026-05-25 22:33:44 +02:00
Paul Bottein 765b73748b Use sefixtures 2026-05-25 22:32:02 +02:00
Paul Bottein afabadb7ff Use sefixtures 2026-05-25 22:24:23 +02:00
Paul Bottein 714be473f8 Add tests 2026-05-25 22:17:42 +02:00
Paul Bottein 09fb62d833 Add delay between commands for novy hood 2026-05-25 22:17:00 +02:00
4 changed files with 70 additions and 19 deletions
@@ -1,5 +1,6 @@
"""Fan platform for the Novy Cooker Hood (calibrated speed control)."""
import asyncio
import math
from typing import Any
@@ -25,6 +26,11 @@ PARALLEL_UPDATES = 1
_SPEED_RANGE = (1, SPEED_COUNT)
# Minimum gap the hood needs to register consecutive presses as distinct
# button events. Without it, low-latency transmitters collapse rapid presses
# into a single one.
_COMMAND_DELAY = 0.5
async def async_setup_entry(
hass: HomeAssistant,
@@ -104,8 +110,7 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
"""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._async_send(plus)
await self._async_send_repeated(plus, steps)
self._level = min(SPEED_COUNT, self._level + steps)
self.async_write_ha_state()
@@ -113,8 +118,7 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
"""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._async_send(minus)
await self._async_send_repeated(minus, steps)
self._level = max(0, self._level - steps)
self.async_write_ha_state()
@@ -128,15 +132,23 @@ class NovyCookerHoodFan(NovyCookerHoodEntity, FanEntity, RestoreEntity):
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._async_send(minus)
await self._async_send_repeated(minus, SPEED_COUNT)
if level > 0:
await asyncio.sleep(_COMMAND_DELAY)
plus = NovyCookerHoodButton.PLUS.to_command(channel=self._code)
for _ in range(level):
await self._async_send(plus)
await self._async_send_repeated(plus, level)
self._level = level
self.async_write_ha_state()
async def _async_send_repeated(
self, command: NovyCookerHoodCommand, count: int
) -> None:
"""Send the same RF command N times, pausing between presses."""
for i in range(count):
if i > 0:
await asyncio.sleep(_COMMAND_DELAY)
await self._async_send(command)
async def _async_send(self, command: NovyCookerHoodCommand) -> None:
"""Send a single RF command via the configured transmitter."""
await async_send_command(
@@ -1,5 +1,8 @@
"""Common fixtures for the Novy Cooker Hood tests."""
from collections.abc import Iterator
from unittest.mock import patch
import pytest
from homeassistant.components.novy_cooker_hood.const import CONF_TRANSMITTER, DOMAIN
@@ -13,6 +16,13 @@ from tests.components.radio_frequency.common import MockRadioFrequencyEntity
TRANSMITTER_ENTITY_ID = "radio_frequency.test_rf_transmitter"
@pytest.fixture(autouse=True)
def mock_command_delay() -> Iterator[None]:
"""Drop the inter-command delay so tests don't spend real time waiting."""
with patch("homeassistant.components.novy_cooker_hood.fan._COMMAND_DELAY", 0):
yield
@pytest.fixture
def mock_config_entry(
mock_rf_entity: MockRadioFrequencyEntity,
+37 -8
View File
@@ -1,5 +1,9 @@
"""Tests for the Novy Hood fan platform."""
from unittest.mock import AsyncMock, call, patch
import pytest
from homeassistant.components.fan import (
ATTR_PERCENTAGE,
ATTR_PERCENTAGE_STEP,
@@ -19,10 +23,10 @@ from tests.components.radio_frequency.common import MockRadioFrequencyEntity
ENTITY_ID = "fan.novy_cooker_hood"
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_turn_on_calibrates_to_level_1(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""Default turn_on sends 4 minus + 1 plus and lands at 25%."""
state = hass.states.get(ENTITY_ID)
@@ -47,10 +51,10 @@ async def test_turn_on_calibrates_to_level_1(
assert all(c.context is context for c in mock_rf_entity.send_command_calls)
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_turn_on_with_percentage_calibrates_to_level(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""turn_on with percentage targets the matching level via calibration."""
await hass.services.async_call(
@@ -67,10 +71,10 @@ async def test_turn_on_with_percentage_calibrates_to_level(
assert len(mock_rf_entity.send_command_calls) == 6
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_set_percentage_zero_turns_off(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""set_percentage(0) turns the fan off via the calibration sequence."""
await hass.services.async_call(
@@ -87,10 +91,10 @@ async def test_set_percentage_zero_turns_off(
assert len(mock_rf_entity.send_command_calls) == 4
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_turn_off_sends_four_minuses(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""turn_off sends 4 minus presses."""
await hass.services.async_call(
@@ -107,10 +111,10 @@ async def test_turn_off_sends_four_minuses(
assert len(mock_rf_entity.send_command_calls) == 4
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_set_percentage_calibrates(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""set_percentage(75) sends 4 minus + 3 plus and lands at level 3."""
await hass.services.async_call(
@@ -127,10 +131,10 @@ async def test_set_percentage_calibrates(
assert len(mock_rf_entity.send_command_calls) == 7
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_increase_speed_sends_single_plus(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""increase_speed sends one plus and bumps level by one (no recalibration)."""
await hass.services.async_call(
@@ -199,10 +203,10 @@ async def test_decrease_speed_sends_single_minus(
assert len(mock_rf_entity.send_command_calls) == 1
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_increase_speed_with_step_sends_n_presses(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""increase_speed with percentage_step sends N plus presses (no recalibration)."""
await hass.services.async_call(
@@ -244,10 +248,10 @@ async def test_decrease_speed_with_step_sends_n_presses(
assert len(mock_rf_entity.send_command_calls) == 2
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_decrease_speed_clamps_at_off(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""decrease_speed at level 0 still sends one minus but level stays at 0."""
await hass.services.async_call(
@@ -263,6 +267,31 @@ async def test_decrease_speed_clamps_at_off(
assert len(mock_rf_entity.send_command_calls) == 1
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_set_percentage_sleeps_between_presses(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
) -> None:
"""A delay is awaited between every RF press, including between sequences."""
delay = 0.5
with (
patch("homeassistant.components.novy_cooker_hood.fan._COMMAND_DELAY", delay),
patch(
"homeassistant.components.novy_cooker_hood.fan.asyncio.sleep",
new_callable=AsyncMock,
) as mock_sleep,
):
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PERCENTAGE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PERCENTAGE: 75},
blocking=True,
)
assert len(mock_rf_entity.send_command_calls) == 7
assert mock_sleep.await_args_list == [call(delay)] * 6
async def test_restore_state(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
@@ -1,5 +1,6 @@
"""Tests for the Novy Hood light platform."""
import pytest
from rf_protocols.codes.novy.cooker_hood import NovyCookerHoodButton
from homeassistant.components.light import (
@@ -25,10 +26,10 @@ from tests.components.radio_frequency.common import MockRadioFrequencyEntity
ENTITY_ID = "light.novy_cooker_hood_light"
@pytest.mark.usefixtures("init_novy_cooker_hood")
async def test_turn_on_and_off_send_light_once_each(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""Turn on sends a light toggle and flips is_on; turn off does the same."""
state = hass.states.get(ENTITY_ID)
@@ -87,10 +88,9 @@ async def test_restore_state(
assert state.state == STATE_ON
@pytest.mark.usefixtures("mock_rf_entity", "init_novy_cooker_hood")
async def test_entity_follows_transmitter_availability(
hass: HomeAssistant,
mock_rf_entity: MockRadioFrequencyEntity,
init_novy_cooker_hood: MockConfigEntry,
) -> None:
"""The light becomes unavailable when the transmitter does, and back."""
await assert_availability_follows_source_entity(