Skip to content

Commit

Permalink
Merge branch 'parameter_refresh_fallback'
Browse files Browse the repository at this point in the history
  • Loading branch information
denpamusic committed Nov 24, 2024
2 parents 4a637a3 + a15036e commit 584b128
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 43 deletions.
32 changes: 31 additions & 1 deletion pyplumio/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType
from pyplumio.exceptions import UnknownDeviceError
from pyplumio.frames import DataFrameDescription, Frame, Request
from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
from pyplumio.helpers.event_manager import EventManager
from pyplumio.helpers.factory import create_instance
from pyplumio.helpers.parameter import Parameter, ParameterValue
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
from pyplumio.structures.network_info import NetworkInfo
from pyplumio.utils import to_camelcase

Expand Down Expand Up @@ -125,11 +126,40 @@ class PhysicalDevice(Device, ABC):
address: ClassVar[int]
_network: NetworkInfo
_setup_frames: tuple[DataFrameDescription, ...]
_frame_versions: dict[int, int]

def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
"""Initialize a new physical device."""
super().__init__(queue)
self._frame_versions = {}
self._network = network
self.subscribe(ATTR_FRAME_VERSIONS, self._update_frame_versions)

async def _update_frame_versions(self, versions: dict[int, int]) -> None:
"""Check frame versions and update outdated frames."""
for frame_type, version in versions.items():
if (
is_known_frame_type(frame_type)
and self.supports_frame_type(frame_type)
and not self.has_frame_version(frame_type, version)
):
request = await Request.create(frame_type, recipient=self.address)
self.queue.put_nowait(request)
self._frame_versions[frame_type] = version

def has_frame_version(self, frame_type: int, version: int | None = None) -> bool:
"""Return True if frame data is up to date, False otherwise."""
if frame_type not in self._frame_versions:
return False

if version is None or self._frame_versions[frame_type] == version:
return True

return False

def supports_frame_type(self, frame_type: int) -> bool:
"""Check if frame type is supported by the device."""
return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])

def handle_frame(self, frame: Frame) -> None:
"""Handle frame received from the device."""
Expand Down
31 changes: 1 addition & 30 deletions pyplumio/devices/ecomax.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import Any, Final

from pyplumio.const import (
ATTR_FRAME_ERRORS,
ATTR_PASSWORD,
ATTR_SENSORS,
ATTR_STATE,
Expand All @@ -21,7 +20,7 @@
from pyplumio.devices.mixer import Mixer
from pyplumio.devices.thermostat import Thermostat
from pyplumio.filters import on_change
from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
from pyplumio.frames import DataFrameDescription, Frame, Request
from pyplumio.helpers.parameter import ParameterValues
from pyplumio.helpers.schedule import Schedule, ScheduleDay
from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
Expand All @@ -35,7 +34,6 @@
EcomaxSwitch,
EcomaxSwitchDescription,
)
from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
Expand Down Expand Up @@ -106,17 +104,14 @@ class EcoMAX(PhysicalDevice):

address = DeviceType.ECOMAX

_frame_versions: dict[int, int]
_fuel_burned_timestamp_ns: int
_setup_frames = SETUP_FRAME_TYPES

def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
"""Initialize a new ecoMAX controller."""
super().__init__(queue, network)
self._frame_versions = {}
self._fuel_burned_timestamp_ns = time.perf_counter_ns()
self.subscribe(ATTR_ECOMAX_PARAMETERS, self._handle_ecomax_parameters)
self.subscribe(ATTR_FRAME_VERSIONS, self._update_frame_versions)
self.subscribe(ATTR_FUEL_CONSUMPTION, self._add_burned_fuel_counter)
self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
Expand All @@ -142,17 +137,6 @@ def handle_frame(self, frame: Frame) -> None:

super().handle_frame(frame)

def _has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
"""Check if ecoMAX controller has this version of the frame."""
return (
frame_type in self._frame_versions
and self._frame_versions[frame_type] == version
)

def _frame_is_supported(self, frame_type: FrameType | int) -> bool:
"""Check if frame is supported by the device."""
return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])

def _mixers(self, indexes: Iterable[int]) -> Generator[Mixer, None, None]:
"""Iterate through the mixer indexes.
Expand Down Expand Up @@ -224,19 +208,6 @@ def _ecomax_parameter_events() -> Generator[Coroutine, Any, None]:
await asyncio.gather(*_ecomax_parameter_events())
return True

async def _update_frame_versions(self, versions: dict[int, int]) -> None:
"""Check frame versions and update outdated frames."""
for frame_type, version in versions.items():
if (
is_known_frame_type(frame_type)
and self._frame_is_supported(frame_type)
and not self._has_frame_version(frame_type, version)
):
# We don't have this frame or it's version has changed.
request = await Request.create(frame_type, recipient=self.address)
self.queue.put_nowait(request)
self._frame_versions[frame_type] = version

async def _add_burned_fuel_counter(self, fuel_consumption: float) -> None:
"""Calculate fuel burned since last sensor's data message."""
current_timestamp_ns = time.perf_counter_ns()
Expand Down
40 changes: 38 additions & 2 deletions pyplumio/helpers/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,19 @@ class ParameterDescription:
class Parameter(ABC):
"""Represents a base parameter."""

__slots__ = ("device", "description", "_pending_update", "_index", "_values")
__slots__ = (
"device",
"description",
"_pending_update",
"_previous_value",
"_index",
"_values",
)

device: Device
description: ParameterDescription
_pending_update: bool
_previous_value: int
_index: int
_values: ParameterValues

Expand All @@ -96,6 +104,7 @@ def __init__(
self.device = device
self.description = description
self._pending_update = False
self._previous_value = 0
self._index = index
self._values = values if values else ParameterValues(0, 0, 0)

Expand Down Expand Up @@ -185,6 +194,7 @@ async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
f"Value must be between '{self.min_value}' and '{self.max_value}'"
)

self._previous_value = self._values.value
self._values.value = value
self._pending_update = True
while self.pending_update:
Expand All @@ -196,15 +206,29 @@ async def set(self, value: Any, retries: int = 5, timeout: float = 5.0) -> bool:
return False

await self.device.queue.put(await self.create_request())
if not self.is_tracking_changes:
await self.force_refresh()

await asyncio.sleep(timeout)
retries -= 1

return True

def update(self, values: ParameterValues) -> None:
"""Update the parameter values."""
if self.pending_update and self._previous_value != values.value:
self._pending_update = False

self._values = values
self._pending_update = False

async def force_refresh(self) -> None:
"""Refresh the parameter from remote."""
await self.device.queue.put(await self.create_refresh_request())

@property
def is_tracking_changes(self) -> bool:
"""Return True if remote's tracking changes, False otherwise."""
return False

@property
def pending_update(self) -> bool:
Expand Down Expand Up @@ -254,6 +278,10 @@ def max_value(self) -> Any:
async def create_request(self) -> Request:
"""Create a request to change the parameter."""

@abstractmethod
async def create_refresh_request(self) -> Request:
"""Create a request to refresh the parameter."""


@dataslots
@dataclass
Expand Down Expand Up @@ -286,6 +314,10 @@ async def create_request(self) -> Request:
"""Create a request to change the number."""
return Request()

async def create_refresh_request(self) -> Request:
"""Create a request to refresh the number."""
return Request()

@property
def value(self) -> int | float:
"""Return the value."""
Expand Down Expand Up @@ -362,6 +394,10 @@ async def create_request(self) -> Request:
"""Create a request to change the switch."""
return Request()

async def create_refresh_request(self) -> Request:
"""Create a request to refresh the switch."""
return Request()

@property
def value(self) -> Literal["off", "on"]:
"""Return the value."""
Expand Down
19 changes: 16 additions & 3 deletions pyplumio/structures/ecomax_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import Generator
from dataclasses import dataclass
from functools import partial
from typing import Any, Final
from typing import TYPE_CHECKING, Any, Final

from dataslots import dataslots

Expand All @@ -19,7 +19,6 @@
ProductType,
UnitOfMeasurement,
)
from pyplumio.devices import PhysicalDevice
from pyplumio.frames import Request
from pyplumio.helpers.parameter import (
Number,
Expand All @@ -35,6 +34,9 @@
from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PROFILE
from pyplumio.utils import ensure_dict

if TYPE_CHECKING:
from pyplumio.devices.ecomax import EcoMAX

ATTR_ECOMAX_CONTROL: Final = "ecomax_control"
ATTR_ECOMAX_PARAMETERS: Final = "ecomax_parameters"

Expand All @@ -53,7 +55,7 @@ class EcomaxParameter(Parameter):

__slots__ = ()

device: PhysicalDevice
device: EcoMAX
description: EcomaxParameterDescription

async def create_request(self) -> Request:
Expand Down Expand Up @@ -81,6 +83,17 @@ async def create_request(self) -> Request:
data={ATTR_INDEX: self._index, ATTR_VALUE: self.values.value},
)

async def create_refresh_request(self) -> Request:
"""Create a request to refresh the parameter."""
return await Request.create(
FrameType.REQUEST_ECOMAX_PARAMETERS, recipient=self.device.address
)

@property
def is_tracking_changes(self) -> bool:
"""Return True if remote's tracking changes, False otherwise."""
return self.device.has_frame_version(FrameType.REQUEST_ECOMAX_PARAMETERS)


@dataslots
@dataclass
Expand Down
11 changes: 11 additions & 0 deletions pyplumio/structures/mixer_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ async def create_request(self) -> Request:
},
)

async def create_refresh_request(self) -> Request:
"""Create a request to refresh the parameter."""
return await Request.create(
FrameType.REQUEST_MIXER_PARAMETERS, recipient=self.device.parent.address
)

@property
def is_tracking_changes(self) -> bool:
"""Return True if remote's tracking changes, False otherwise."""
return self.device.parent.has_frame_version(FrameType.REQUEST_MIXER_PARAMETERS)


@dataslots
@dataclass
Expand Down
11 changes: 11 additions & 0 deletions pyplumio/structures/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ async def create_request(self) -> Request:
data=collect_schedule_data(schedule_name, self.device),
)

async def create_refresh_request(self) -> Request:
"""Create a request to refresh the parameter."""
return await Request.create(
FrameType.REQUEST_SCHEDULES, recipient=self.device.address
)

@property
def is_tracking_changes(self) -> bool:
"""Return True if remote's tracking changes, False otherwise."""
return self.device.has_frame_version(FrameType.REQUEST_SCHEDULES)


@dataslots
@dataclass
Expand Down
14 changes: 14 additions & 0 deletions pyplumio/structures/thermostat_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ async def create_request(self) -> Request:
},
)

async def create_refresh_request(self) -> Request:
"""Create a request to refresh the parameter."""
return await Request.create(
FrameType.REQUEST_THERMOSTAT_PARAMETERS,
recipient=self.device.parent.address,
)

@property
def is_tracking_changes(self) -> bool:
"""Return True if remote's tracking changes, False otherwise."""
return self.device.parent.has_frame_version(
FrameType.REQUEST_THERMOSTAT_PARAMETERS
)


@dataslots
@dataclass
Expand Down
22 changes: 18 additions & 4 deletions tests/helpers/test_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,14 @@ async def test_switch_value(switch: Switch) -> None:

async def test_number_set(number: Number, bypass_asyncio_sleep) -> None:
"""Test setting a number."""
await number.set(5)
retries = 5
with patch(
"pyplumio.helpers.parameter.Number.create_refresh_request"
) as mock_create_refresh_request:
await number.set(5, retries=retries)

assert not number.is_tracking_changes
assert mock_create_refresh_request.await_count == retries
number.update(ParameterValues(value=5, min_value=0, max_value=5))
assert number == 5
assert not number.pending_update
Expand All @@ -127,7 +134,14 @@ async def test_number_set(number: Number, bypass_asyncio_sleep) -> None:

async def test_switch_set(switch: Switch, bypass_asyncio_sleep) -> None:
"""Test setting a number."""
await switch.set(STATE_ON)
retries = 5
with patch(
"pyplumio.helpers.parameter.Switch.create_refresh_request"
) as mock_create_refresh_request:
await switch.set(STATE_ON, retries=retries)

assert not switch.is_tracking_changes
assert mock_create_refresh_request.await_count == retries
switch.update(ParameterValues(value=1, min_value=0, max_value=1))
assert switch == 1
assert not switch.pending_update
Expand Down Expand Up @@ -261,7 +275,7 @@ async def test_number_request_with_unchanged_value(
assert not number.pending_update
assert not await number.set(5, retries=3)
assert number.pending_update
assert mock_put.await_count == 3 # type: ignore [unreachable]
assert mock_put.await_count == 6 # type: ignore [unreachable]
mock_put.reset_mock()
assert "Timed out while trying to set 'test_number' parameter" in caplog.text
await number.set(5)
Expand All @@ -276,7 +290,7 @@ async def test_switch_request_with_unchanged_value(
assert not switch.pending_update
assert not await switch.set(True, retries=3)
assert switch.pending_update
assert mock_put.await_count == 3 # type: ignore [unreachable]
assert mock_put.await_count == 6 # type: ignore [unreachable]
mock_put.reset_mock()
assert "Timed out while trying to set 'test_switch' parameter" in caplog.text
await switch.set(True)
Expand Down
Loading

0 comments on commit 584b128

Please sign in to comment.