diff --git a/src/haclient/domains/air_quality.py b/src/haclient/domains/air_quality.py index 6e5b06e..c4a887d 100644 --- a/src/haclient/domains/air_quality.py +++ b/src/haclient/domains/air_quality.py @@ -5,7 +5,7 @@ from typing import Any from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler def _coerce_numeric(value: Any) -> float | int | None: @@ -63,7 +63,7 @@ class AirQuality(Entity): # -- Listener decorators ------------------------------------------ - def on_aqi_change(self, func: Any) -> Any: + def on_aqi_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for Air Quality Index changes. Fires whenever the entity *state* string changes, which mirrors @@ -81,7 +81,7 @@ def on_aqi_change(self, func: Any) -> Any: """ return self._register_state_value_listener(func) - def on_pm25_change(self, func: Any) -> Any: + def on_pm25_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for PM2.5 attribute changes. Parameters @@ -97,7 +97,7 @@ def on_pm25_change(self, func: Any) -> Any: """ return self._register_attr_listener("particulate_matter_2_5", func) - def on_co2_change(self, func: Any) -> Any: + def on_co2_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for CO2 attribute changes. Parameters diff --git a/src/haclient/domains/binary_sensor.py b/src/haclient/domains/binary_sensor.py index c03bd25..9bb0b24 100644 --- a/src/haclient/domains/binary_sensor.py +++ b/src/haclient/domains/binary_sensor.py @@ -2,10 +2,8 @@ from __future__ import annotations -from typing import Any - from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler class BinarySensor(Entity): @@ -20,14 +18,14 @@ class BinarySensor(Entity): # -- Listener decorators ------------------------------------------ - def on_activate(self, func: Any) -> Any: + def on_activate(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the sensor activates (state ``on``). Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``on`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``on`` state. Returns ------- @@ -37,14 +35,14 @@ def on_activate(self, func: Any) -> Any: """ return self._register_state_transition_listener("on", func) - def on_deactivate(self, func: Any) -> Any: + def on_deactivate(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the sensor deactivates (state ``off``). Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``off`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``off`` state. Returns ------- diff --git a/src/haclient/domains/climate.py b/src/haclient/domains/climate.py index 9f335b0..9b51b34 100644 --- a/src/haclient/domains/climate.py +++ b/src/haclient/domains/climate.py @@ -5,7 +5,7 @@ from typing import Any from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler class Climate(Entity): @@ -19,13 +19,14 @@ class Climate(Entity): # -- Listener decorators ------------------------------------------ - def on_hvac_mode_change(self, func: Any) -> Any: + def on_hvac_mode_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for HVAC mode changes. Parameters ---------- func : callable - Callable receiving the new HVAC mode string (e.g. ``"heat"``). + Sync or async callable invoked with ``(old_state, new_state)`` + HVAC mode strings (e.g. ``("cool", "heat")``). Returns ------- @@ -34,13 +35,14 @@ def on_hvac_mode_change(self, func: Any) -> Any: """ return self._register_state_value_listener(func) - def on_temperature_change(self, func: Any) -> Any: + def on_temperature_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for current temperature changes. Parameters ---------- func : callable - Callable receiving the new ``current_temperature`` value. + Callable invoked with ``(old_value, new_value)`` whenever + the ``current_temperature`` attribute changes. Returns ------- @@ -49,13 +51,14 @@ def on_temperature_change(self, func: Any) -> Any: """ return self._register_attr_listener("current_temperature", func) - def on_target_temperature_change(self, func: Any) -> Any: + def on_target_temperature_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for target temperature changes. Parameters ---------- func : callable - Callable receiving the new target temperature value. + Callable invoked with ``(old_value, new_value)`` whenever + the ``temperature`` (target temperature) attribute changes. Returns ------- diff --git a/src/haclient/domains/cover.py b/src/haclient/domains/cover.py index 0abb5f7..539ec9e 100644 --- a/src/haclient/domains/cover.py +++ b/src/haclient/domains/cover.py @@ -2,10 +2,8 @@ from __future__ import annotations -from typing import Any - from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler class Cover(Entity): @@ -19,14 +17,14 @@ class Cover(Entity): # -- Listener decorators ------------------------------------------ - def on_open(self, func: Any) -> Any: + def on_open(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the cover opens. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``open`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``open`` state. Returns ------- @@ -35,14 +33,14 @@ def on_open(self, func: Any) -> Any: """ return self._register_state_transition_listener("open", func) - def on_close(self, func: Any) -> Any: + def on_close(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the cover closes. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``closed`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``closed`` state. Returns ------- @@ -51,14 +49,14 @@ def on_close(self, func: Any) -> Any: """ return self._register_state_transition_listener("closed", func) - def on_position_change(self, func: Any) -> Any: + def on_position_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for position changes. Parameters ---------- func : callable - Callable receiving the new ``current_position`` value - (0-100). + Callable invoked with ``(old_value, new_value)`` whenever + the ``current_position`` attribute (0-100) changes. Returns ------- diff --git a/src/haclient/domains/fan.py b/src/haclient/domains/fan.py index 372cd7a..9b057a0 100644 --- a/src/haclient/domains/fan.py +++ b/src/haclient/domains/fan.py @@ -3,10 +3,9 @@ from __future__ import annotations import logging -from typing import Any from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler _LOGGER = logging.getLogger(__name__) @@ -44,7 +43,7 @@ class Fan(Entity): # -- Listener decorators ------------------------------------------ - def on_turn_on(self, func: Any) -> Any: + def on_turn_on(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the fan turns on. Parameters @@ -60,7 +59,7 @@ def on_turn_on(self, func: Any) -> Any: """ return self._register_state_transition_listener("on", func) - def on_turn_off(self, func: Any) -> Any: + def on_turn_off(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the fan turns off. Parameters @@ -76,7 +75,7 @@ def on_turn_off(self, func: Any) -> Any: """ return self._register_state_transition_listener("off", func) - def on_speed_change(self, func: Any) -> Any: + def on_speed_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for fan speed (``percentage``) changes. Parameters @@ -92,7 +91,7 @@ def on_speed_change(self, func: Any) -> Any: """ return self._register_attr_listener("percentage", func) - def on_direction_change(self, func: Any) -> Any: + def on_direction_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for fan direction changes. Parameters diff --git a/src/haclient/domains/humidifier.py b/src/haclient/domains/humidifier.py index f501947..290dbff 100644 --- a/src/haclient/domains/humidifier.py +++ b/src/haclient/domains/humidifier.py @@ -2,10 +2,8 @@ from __future__ import annotations -from typing import Any - from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler class Humidifier(Entity): @@ -26,14 +24,14 @@ class Humidifier(Entity): # -- Listener decorators ------------------------------------------ - def on_turn_on(self, func: Any) -> Any: + def on_turn_on(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the humidifier turns on. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``on`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``on`` state. Returns ------- @@ -42,14 +40,14 @@ def on_turn_on(self, func: Any) -> Any: """ return self._register_state_transition_listener("on", func) - def on_turn_off(self, func: Any) -> Any: + def on_turn_off(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the humidifier turns off. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``off`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``off`` state. Returns ------- @@ -58,13 +56,14 @@ def on_turn_off(self, func: Any) -> Any: """ return self._register_state_transition_listener("off", func) - def on_humidity_change(self, func: Any) -> Any: + def on_humidity_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for target humidity changes. Parameters ---------- func : callable - Callable receiving the new target ``humidity`` value. + Callable invoked with ``(old_value, new_value)`` whenever + the target ``humidity`` attribute changes. Returns ------- @@ -73,13 +72,14 @@ def on_humidity_change(self, func: Any) -> Any: """ return self._register_attr_listener("humidity", func) - def on_mode_change(self, func: Any) -> Any: + def on_mode_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for operating mode changes. Parameters ---------- func : callable - Callable receiving the new mode string. + Callable invoked with ``(old_value, new_value)`` whenever + the ``mode`` attribute changes. Returns ------- diff --git a/src/haclient/domains/light.py b/src/haclient/domains/light.py index b4cf07d..04d73fe 100644 --- a/src/haclient/domains/light.py +++ b/src/haclient/domains/light.py @@ -5,7 +5,7 @@ from typing import Any from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler class Light(Entity): @@ -23,14 +23,14 @@ class Light(Entity): # -- Listener decorators ------------------------------------------ - def on_turn_on(self, func: Any) -> Any: + def on_turn_on(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the light turns on. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``on`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``on`` state. Returns ------- @@ -39,14 +39,14 @@ def on_turn_on(self, func: Any) -> Any: """ return self._register_state_transition_listener("on", func) - def on_turn_off(self, func: Any) -> Any: + def on_turn_off(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the light turns off. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``off`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``off`` state. Returns ------- @@ -55,13 +55,14 @@ def on_turn_off(self, func: Any) -> Any: """ return self._register_state_transition_listener("off", func) - def on_brightness_change(self, func: Any) -> Any: + def on_brightness_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for brightness changes. Parameters ---------- func : callable - Callable receiving the new brightness value (0-255). + Callable invoked with ``(old_value, new_value)`` whenever + the ``brightness`` attribute (0-255) changes. Returns ------- @@ -70,14 +71,15 @@ def on_brightness_change(self, func: Any) -> Any: """ return self._register_attr_listener("brightness", func) - def on_color_change(self, func: Any) -> Any: + def on_color_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for RGB color changes. Parameters ---------- func : callable - Callable receiving the new ``rgb_color`` value as reported - by Home Assistant. + Callable invoked with ``(old_value, new_value)`` whenever + the ``rgb_color`` attribute reported by Home Assistant + changes. Returns ------- @@ -86,13 +88,14 @@ def on_color_change(self, func: Any) -> Any: """ return self._register_attr_listener("rgb_color", func) - def on_kelvin_change(self, func: Any) -> Any: + def on_kelvin_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for color temperature (Kelvin) changes. Parameters ---------- func : callable - Callable receiving the new ``color_temp_kelvin`` value. + Callable invoked with ``(old_value, new_value)`` whenever + the ``color_temp_kelvin`` attribute changes. Returns ------- diff --git a/src/haclient/domains/lock.py b/src/haclient/domains/lock.py index a154859..4d63dc7 100644 --- a/src/haclient/domains/lock.py +++ b/src/haclient/domains/lock.py @@ -3,10 +3,9 @@ from __future__ import annotations import logging -from typing import Any from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler _LOGGER = logging.getLogger(__name__) @@ -35,14 +34,14 @@ class Lock(Entity): # -- Listener decorators ------------------------------------------ - def on_lock(self, func: Any) -> Any: + def on_lock(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the lock becomes locked. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``locked`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``locked`` state. Returns ------- @@ -51,14 +50,14 @@ def on_lock(self, func: Any) -> Any: """ return self._register_state_transition_listener("locked", func) - def on_unlock(self, func: Any) -> Any: + def on_unlock(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the lock becomes unlocked. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``unlocked`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``unlocked`` state. Returns ------- @@ -67,14 +66,14 @@ def on_unlock(self, func: Any) -> Any: """ return self._register_state_transition_listener("unlocked", func) - def on_jam(self, func: Any) -> Any: + def on_jam(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the lock jams. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``jammed`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``jammed`` state. Returns ------- diff --git a/src/haclient/domains/media_player.py b/src/haclient/domains/media_player.py index 5406dfb..c42a810 100644 --- a/src/haclient/domains/media_player.py +++ b/src/haclient/domains/media_player.py @@ -236,14 +236,14 @@ def __init__( # -- Listener decorators ------------------------------------------ - def on_volume_change(self, func: Any) -> Any: + def on_volume_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for volume level changes. Parameters ---------- func : callable - Callable receiving the new ``volume_level`` value - (``0.0``-``1.0``). + Callable invoked with ``(old_value, new_value)`` whenever + the ``volume_level`` attribute (``0.0``-``1.0``) changes. Returns ------- @@ -252,13 +252,14 @@ def on_volume_change(self, func: Any) -> Any: """ return self._register_attr_listener("volume_level", func) - def on_mute_change(self, func: Any) -> Any: + def on_mute_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for mute state changes. Parameters ---------- func : callable - Callable receiving the new ``is_volume_muted`` value. + Callable invoked with ``(old_value, new_value)`` whenever + the ``is_volume_muted`` attribute changes. Returns ------- @@ -267,16 +268,14 @@ def on_mute_change(self, func: Any) -> Any: """ return self._register_attr_listener("is_volume_muted", func) - def on_media_change(self, func: Any) -> Any: + def on_media_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the playing media changes. - Receives ``(old: NowPlaying, new: NowPlaying)``. - Parameters ---------- func : callable - Callable invoked with the previous and current `NowPlaying` - snapshots whenever they differ. + Callable invoked with ``(old, new)`` where both arguments + are `NowPlaying` snapshots whenever they differ. Returns ------- @@ -286,14 +285,14 @@ def on_media_change(self, func: Any) -> Any: self._media_change_listeners.append(func) return func - def on_play(self, func: Any) -> Any: + def on_play(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when playback starts. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``playing`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``playing`` state. Returns ------- @@ -302,14 +301,14 @@ def on_play(self, func: Any) -> Any: """ return self._register_state_transition_listener("playing", func) - def on_pause(self, func: Any) -> Any: + def on_pause(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when playback pauses. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``paused`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``paused`` state. Returns ------- @@ -318,14 +317,14 @@ def on_pause(self, func: Any) -> Any: """ return self._register_state_transition_listener("paused", func) - def on_stop(self, func: Any) -> Any: + def on_stop(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when playback stops. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``idle`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``idle`` state. Returns ------- diff --git a/src/haclient/domains/scene.py b/src/haclient/domains/scene.py index d513f1d..0d29085 100644 --- a/src/haclient/domains/scene.py +++ b/src/haclient/domains/scene.py @@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Any from haclient.core.plugins import DomainAccessor, DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler if TYPE_CHECKING: from haclient.core.factory import EntityFactory @@ -90,14 +90,15 @@ async def delete(self) -> None: # -- Listener decorators ------------------------------------------ - def on_activate(self, func: Any) -> Any: + def on_activate(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener that fires when the scene is activated. Parameters ---------- func : callable - Sync or async callable receiving the new ``state`` value - (the ISO-8601 activation timestamp). + Sync or async callable invoked with ``(old_state, new_state)`` + ISO-8601 activation-timestamp strings whenever the scene is + re-activated. Returns ------- diff --git a/src/haclient/domains/sensor.py b/src/haclient/domains/sensor.py index af0cbd9..6f35c89 100644 --- a/src/haclient/domains/sensor.py +++ b/src/haclient/domains/sensor.py @@ -2,10 +2,8 @@ from __future__ import annotations -from typing import Any - from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler class Sensor(Entity): @@ -20,15 +18,15 @@ class Sensor(Entity): # -- Listener decorators ------------------------------------------ - def on_value_change(self, func: Any) -> Any: + def on_value_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for sensor value changes. - Receives the **state strings** directly (e.g. ``"21.5"``). - Parameters ---------- func : callable - Sync or async callable receiving the new state string. + Sync or async callable invoked with the previous and current + sensor **state strings** as ``(old_value, new_value)`` + (e.g. ``("21.5", "22.0")``). Returns ------- diff --git a/src/haclient/domains/switch.py b/src/haclient/domains/switch.py index 1da8ead..2b5f970 100644 --- a/src/haclient/domains/switch.py +++ b/src/haclient/domains/switch.py @@ -2,10 +2,8 @@ from __future__ import annotations -from typing import Any - from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler class Switch(Entity): @@ -20,14 +18,14 @@ class Switch(Entity): # -- Listener decorators ------------------------------------------ - def on_turn_on(self, func: Any) -> Any: + def on_turn_on(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the switch turns on. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``on`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``on`` state. Returns ------- @@ -36,14 +34,14 @@ def on_turn_on(self, func: Any) -> Any: """ return self._register_state_transition_listener("on", func) - def on_turn_off(self, func: Any) -> Any: + def on_turn_off(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the switch turns off. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``off`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``off`` state. Returns ------- diff --git a/src/haclient/domains/timer.py b/src/haclient/domains/timer.py index 9bbfec6..8f9a60c 100644 --- a/src/haclient/domains/timer.py +++ b/src/haclient/domains/timer.py @@ -228,14 +228,14 @@ async def change(self, *, duration: str) -> None: # -- Listener decorators ------------------------------------------ - def on_start(self, func: Any) -> Any: + def on_start(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the timer starts (becomes active). Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``active`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``active`` state. Returns ------- @@ -244,14 +244,14 @@ def on_start(self, func: Any) -> Any: """ return self._register_state_transition_listener("active", func) - def on_pause(self, func: Any) -> Any: + def on_pause(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the timer is paused. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``paused`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``paused`` state. Returns ------- @@ -260,14 +260,14 @@ def on_pause(self, func: Any) -> Any: """ return self._register_state_transition_listener("paused", func) - def on_idle(self, func: Any) -> Any: + def on_idle(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the timer becomes idle. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``idle`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``idle`` state. Returns ------- @@ -276,7 +276,7 @@ def on_idle(self, func: Any) -> Any: """ return self._register_state_transition_listener("idle", func) - def on_finished(self, func: Any) -> Any: + def on_finished(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for natural timer expiry. Driven by the HA ``timer.finished`` event (not state changes). @@ -284,8 +284,8 @@ def on_finished(self, func: Any) -> Any: Parameters ---------- func : callable - Callable invoked with ``(entity_id, event_data)`` when the - timer expires. + Sync or async callable invoked with ``(entity_id, event_data)`` + when the timer expires. Returns ------- @@ -295,7 +295,7 @@ def on_finished(self, func: Any) -> Any: self._finished_listeners.append(func) return func - def on_cancelled(self, func: Any) -> Any: + def on_cancelled(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for explicit timer cancellation. Driven by the HA ``timer.cancelled`` event (not state changes). diff --git a/src/haclient/domains/vacuum.py b/src/haclient/domains/vacuum.py index 6f12389..1f1787b 100644 --- a/src/haclient/domains/vacuum.py +++ b/src/haclient/domains/vacuum.py @@ -6,7 +6,7 @@ from typing import Any from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler _LOGGER = logging.getLogger(__name__) @@ -59,14 +59,14 @@ class Vacuum(Entity): # -- Listener decorators ------------------------------------------ - def on_start(self, func: Any) -> Any: + def on_start(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the vacuum starts cleaning. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``cleaning`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``cleaning`` state. Returns ------- @@ -75,14 +75,14 @@ def on_start(self, func: Any) -> Any: """ return self._register_state_transition_listener(_STATE_CLEANING, func) - def on_dock(self, func: Any) -> Any: + def on_dock(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the vacuum returns to the dock. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``docked`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``docked`` state. Returns ------- @@ -91,14 +91,14 @@ def on_dock(self, func: Any) -> Any: """ return self._register_state_transition_listener(_STATE_DOCKED, func) - def on_error(self, func: Any) -> Any: + def on_error(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the vacuum enters the error state. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``error`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``error`` state. Returns ------- @@ -107,13 +107,14 @@ def on_error(self, func: Any) -> Any: """ return self._register_state_transition_listener(_STATE_ERROR, func) - def on_battery_change(self, func: Any) -> Any: + def on_battery_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for battery level changes. Parameters ---------- func : callable - Callable receiving the new ``battery_level`` value (0--100). + Callable invoked with ``(old_value, new_value)`` whenever + the ``battery_level`` attribute (0-100) changes. Returns ------- @@ -122,13 +123,14 @@ def on_battery_change(self, func: Any) -> Any: """ return self._register_attr_listener("battery_level", func) - def on_fan_speed_change(self, func: Any) -> Any: + def on_fan_speed_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for fan-speed changes. Parameters ---------- func : callable - Callable receiving the new ``fan_speed`` value. + Callable invoked with ``(old_value, new_value)`` whenever + the ``fan_speed`` attribute changes. Returns ------- diff --git a/src/haclient/domains/valve.py b/src/haclient/domains/valve.py index 5898f18..7b895ef 100644 --- a/src/haclient/domains/valve.py +++ b/src/haclient/domains/valve.py @@ -3,10 +3,9 @@ from __future__ import annotations import logging -from typing import Any from haclient.core.plugins import DomainSpec, register_domain -from haclient.entity.base import Entity +from haclient.entity.base import Entity, ValueChangeHandler _LOGGER = logging.getLogger(__name__) @@ -41,14 +40,14 @@ class Valve(Entity): # -- Listener decorators ------------------------------------------ - def on_open(self, func: Any) -> Any: + def on_open(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the valve opens. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``open`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``open`` state. Returns ------- @@ -57,14 +56,14 @@ def on_open(self, func: Any) -> Any: """ return self._register_state_transition_listener("open", func) - def on_close(self, func: Any) -> Any: + def on_close(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for when the valve closes. Parameters ---------- func : callable - Sync or async zero-argument callable invoked on every - transition into the ``closed`` state. + Sync or async callable invoked with ``(old_state, new_state)`` + on every transition into the ``closed`` state. Returns ------- @@ -73,14 +72,14 @@ def on_close(self, func: Any) -> Any: """ return self._register_state_transition_listener("closed", func) - def on_position_change(self, func: Any) -> Any: + def on_position_change(self, func: ValueChangeHandler) -> ValueChangeHandler: """Register a listener for position changes. Parameters ---------- func : callable - Callable receiving the new ``current_position`` value - (0--100). + Callable invoked with ``(old_value, new_value)`` whenever + the ``current_position`` attribute (0-100) changes. Returns ------- diff --git a/tests/test_granular_events.py b/tests/test_granular_events.py index 8f86153..4ea5da9 100644 --- a/tests/test_granular_events.py +++ b/tests/test_granular_events.py @@ -839,3 +839,154 @@ def on_can(entity_id: Any, data: Any) -> None: await asyncio.sleep(0.05) assert finished == [] assert len(cancelled) == 1 + + +# --------------------------------------------------------------------------- +# Listener-callback contract regression tests (issue #72). +# +# All ``on_*`` listener decorators across every domain dispatch callbacks +# with ``(old, new)``. These tests pin that contract by exercising one +# representative listener of each shape (attribute, state-transition, +# state-value, media-change, timer-event) and by verifying that a +# zero-argument callback — previously implied by some docstrings — is +# rejected by Python at call time, with the error logged and swallowed +# so other handlers continue to run. +# --------------------------------------------------------------------------- + + +async def test_state_transition_callback_receives_old_and_new( + client: HAClient, fake_ha: FakeHA +) -> None: + """``on_turn_on``-style transition callbacks get ``(old_state, new_state)``.""" + light = client.light("kitchen") + received: list[tuple[Any, Any]] = [] + + @light.on_turn_on + def handler(old: Any, new: Any) -> None: + received.append((old, new)) + + await fake_ha.push_state_changed( + "light.kitchen", + {"state": "on", "attributes": {}}, + {"state": "off", "attributes": {}}, + ) + await asyncio.sleep(0.05) + assert received == [("off", "on")] + + +async def test_attr_change_callback_receives_old_and_new(client: HAClient, fake_ha: FakeHA) -> None: + """``on_brightness_change``-style callbacks get ``(old_value, new_value)``.""" + light = client.light("kitchen") + received: list[tuple[Any, Any]] = [] + + @light.on_brightness_change + def handler(old: Any, new: Any) -> None: + received.append((old, new)) + + await fake_ha.push_state_changed( + "light.kitchen", + {"state": "on", "attributes": {"brightness": 200}}, + {"state": "on", "attributes": {"brightness": 100}}, + ) + await asyncio.sleep(0.05) + assert received == [(100, 200)] + + +async def test_state_value_callback_receives_old_and_new(client: HAClient, fake_ha: FakeHA) -> None: + """``on_value_change``-style callbacks get ``(old_state, new_state)``.""" + sensor = client.sensor("temperature") + received: list[tuple[Any, Any]] = [] + + @sensor.on_value_change + def handler(old: Any, new: Any) -> None: + received.append((old, new)) + + await fake_ha.push_state_changed( + "sensor.temperature", + {"state": "22.0", "attributes": {}}, + {"state": "21.5", "attributes": {}}, + ) + await asyncio.sleep(0.05) + assert received == [("21.5", "22.0")] + + +async def test_zero_argument_callback_is_logged_and_skipped( + client: HAClient, fake_ha: FakeHA, caplog: Any +) -> None: + """Zero-argument callbacks (previously documented but unsupported) + raise ``TypeError`` at dispatch time, are logged, and do not break + other handlers registered for the same event. + + This pins the runtime contract: every public ``on_*`` listener + decorator forwards ``(old, new)``. Users registering anything with + fewer than two positional parameters will see a logged error rather + than a silent no-op-shaped success. + """ + light = client.light("kitchen") + good: list[tuple[Any, Any]] = [] + + @light.on_turn_on + def zero_arg_handler() -> None: # type: ignore[misc] + # Intentionally wrong arity to verify behavior on misuse. + pass + + @light.on_turn_on + def good_handler(old: Any, new: Any) -> None: + good.append((old, new)) + + with caplog.at_level("ERROR", logger="haclient.entity.base"): + await fake_ha.push_state_changed( + "light.kitchen", + {"state": "on", "attributes": {}}, + {"state": "off", "attributes": {}}, + ) + await asyncio.sleep(0.05) + + assert good == [("off", "on")] + # The bad handler raised TypeError when called with two args. + assert any( + record.levelname == "ERROR" and "Value change handler raised" in record.message + for record in caplog.records + ) + + +async def test_media_change_callback_receives_old_and_new( + client: HAClient, fake_ha: FakeHA +) -> None: + """``MediaPlayer.on_media_change`` callbacks get ``(old, new)`` NowPlaying.""" + player = client.media_player("living_room") + received: list[tuple[NowPlaying, NowPlaying]] = [] + + @player.on_media_change + def handler(old: NowPlaying, new: NowPlaying) -> None: + received.append((old, new)) + + await fake_ha.push_state_changed( + "media_player.living_room", + {"state": "playing", "attributes": {"media_title": "New"}}, + {"state": "playing", "attributes": {"media_title": "Old"}}, + ) + await asyncio.sleep(0.05) + assert len(received) == 1 + assert received[0][0].title == "Old" + assert received[0][1].title == "New" + + +async def test_timer_event_callback_receives_entity_id_and_data( + client: HAClient, fake_ha: FakeHA +) -> None: + """``Timer.on_finished``/``on_cancelled`` callbacks get ``(entity_id, data)``.""" + t = client.timer("my_timer") + received: list[tuple[Any, Any]] = [] + + @t.on_finished + def handler(entity_id: Any, data: Any) -> None: + received.append((entity_id, data)) + + await fake_ha.push_event( + "timer.finished", + {"data": {"entity_id": "timer.my_timer"}}, + ) + await asyncio.sleep(0.05) + assert len(received) == 1 + assert received[0][0] == "timer.my_timer"