Skip to content

Commit

Permalink
Support setting remaining days on reminders
Browse files Browse the repository at this point in the history
  • Loading branch information
gazoodle committed Feb 17, 2025
1 parent 6ecca83 commit a2e972a
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 69 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ https://www.gnu.org/licenses/gpl-3.0.html
shared global config?

## Done/Fixed in 1.0.3
- Support setting of the remaining duration for reminders

## Done/Fixed in 1.0.2
- Some progress toward support for MrSteam units
Expand Down
24 changes: 24 additions & 0 deletions src/geckolib/async_spa.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import Any

from geckolib.driver.accessor import GeckoStructAccessor
from geckolib.driver.protocol.reminders import GeckoReminderType

from .async_spa_descriptor import GeckoAsyncSpaDescriptor
from .async_taskman import GeckoAsyncTaskMan
Expand Down Expand Up @@ -751,6 +752,29 @@ async def async_get_reminders(self) -> list[tuple]:

return get_reminders_handler.reminders

async def async_set_reminders(
self, reminders: list[tuple[GeckoReminderType, int]]
) -> None:
"""Set the reminders."""
if not self.is_connected:
_LOGGER.warning("Cannot set reminders when spa not connected")
return
if not self.is_responding_to_pings:
_LOGGER.debug("Cannot set reminders when spa not responding to pings")
return
assert self._protocol is not None # noqa: S101
set_reminders_handler = await self._protocol.get(
lambda: GeckoRemindersProtocolHandler.set(
self._protocol.get_and_increment_sequence_counter(),
reminders,
parms=self.sendparms,
)
)

if set_reminders_handler is None:
_LOGGER.error("Cannot set reminders, protocol retry count exceeded")
await self._event_handler(GeckoSpaEvent.ERROR_PROTOCOL_RETRY_COUNT_EXCEEDED)

def get_snapshot_data(self) -> dict:
"""Get the snapshot data for this spa."""
data = self.struct.get_snapshot_data()
Expand Down
67 changes: 29 additions & 38 deletions src/geckolib/automation/reminders.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from datetime import UTC, datetime
from typing import TYPE_CHECKING

from geckolib.driver import GeckoRemindersProtocolHandler, GeckoReminderType
from geckolib.driver import GeckoReminderType

from .base import GeckoAutomationFacadeBase

Expand Down Expand Up @@ -42,6 +42,10 @@ def days(self) -> int:
"""Get the remaining days."""
return self._days

def set_days(self, days: int) -> None:
"""Set the remaining days."""
self._days = days

@property
def monitor(self) -> str:
"""Get the monitor string."""
Expand All @@ -61,24 +65,41 @@ def __init__(self, facade: GeckoAsyncFacade) -> None:
"""Initialize the reminders class."""
super().__init__(facade, "Reminders", "REMINDERS")

self._active_reminders: list[GeckoReminders.Reminder] = []
self._all_reminders: list[GeckoReminders.Reminder] = []
self._reminders_handler = None
self._last_update = None

@property
def reminders(self) -> list[Reminder]:
"""Return all reminders."""
return self._active_reminders
"""Return active reminders."""
return [
reminder
for reminder in self._all_reminders
if reminder.reminder_type != GeckoReminderType.INVALID
]

def get_reminder(
self, reminder_type: GeckoReminderType
) -> GeckoReminders.Reminder | None:
"""Get the reminder of the specified type, or None if not found."""
for reminder in self.reminders:
for reminder in self._all_reminders:
if reminder.reminder_type == reminder_type:
return reminder
return None

async def set_reminder(self, reminder_type: GeckoReminderType, days: int) -> None:
"""Set the remaining days for the specified reminder type."""
for reminder in self._all_reminders:
if reminder.reminder_type == reminder_type:
reminder.set_days(days)
await self.facade.spa.async_set_reminders(
[
(reminder.reminder_type, reminder.days)
for reminder in self._all_reminders
]
)
self._on_change(self)

@property
def last_update(self) -> datetime | None:
"""Time of last reminder update."""
Expand All @@ -87,41 +108,11 @@ def last_update(self) -> datetime | None:
def change_reminders(self, reminders: list[tuple]) -> None:
"""Call from async facade to update active reminders."""
self._last_update = datetime.now(tz=UTC)
self._active_reminders = []
for reminder in reminders:
if reminder[0] != GeckoReminderType.INVALID:
self._active_reminders.append(GeckoReminders.Reminder(reminder))
self._all_reminders = [
GeckoReminders.Reminder(reminder) for reminder in reminders
]
self._on_change(self)

def _on_reminders(
self, handler: GeckoRemindersProtocolHandler, _sender: tuple
) -> None:
"""Call to from protocal handler. Will filter out only the active reminders."""
self._active_reminders = []
if handler.reminders is not None:
# get actual time
now = datetime.now(tz=UTC) # current date and time
time = now.strftime("%d.%m.%Y, %H:%M:%S")
self._active_reminders.append(("Time", time))
for reminder in handler.reminders:
if reminder[0] != GeckoReminderType.INVALID:
self._active_reminders.append(
(GeckoReminderType.to_string(reminder[0]), reminder[1])
)

self._reminders_handler = None

def obsolete_update(self) -> None:
"""Update the reminders."""
self._reminders_handler = GeckoRemindersProtocolHandler.request(
self._spa.get_and_increment_sequence_counter(),
on_handled=self._on_reminders,
parms=self._spa.sendparms,
)

self._spa.add_receive_handler(self._reminders_handler)
self._spa.queue_send(self._reminders_handler, self._spa.sendparms)

def __str__(self) -> str:
"""Stringize the class."""
if self.reminders is None:
Expand Down
69 changes: 57 additions & 12 deletions src/geckolib/driver/protocol/reminders.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

from .packet import GeckoPacketProtocolHandler

REQRM_VERB = b"REQRM"
RMREQ_VERB = b"RMREQ"
REQRM_VERB = b"REQRM" # Request all reminders
RMREQ_VERB = b"RMREQ" # Response with all 10 reminders
SETRM_VERB = b"SETRM" # Set all 10 reminders
RMSET_VERB = b"RMSET" # Ack for the reminder set

RESPONSE_FORMAT = ">BBB"

Expand Down Expand Up @@ -66,7 +68,7 @@ def request(seq: int, **kwargs: Any) -> GeckoRemindersProtocolHandler:
)

@staticmethod
def response(
def req_response(
reminders: list[tuple[GeckoReminderType, int]], **kwargs: Any
) -> GeckoRemindersProtocolHandler:
"""Generate response handler."""
Expand All @@ -81,29 +83,72 @@ def response(
**kwargs,
)

@staticmethod
def set(
seq: int, reminders: list[tuple[GeckoReminderType, int]], **kwargs: Any
) -> GeckoRemindersProtocolHandler:
"""Generate a set command."""
return GeckoRemindersProtocolHandler(
content=b"".join(
[SETRM_VERB, struct.pack(">B", seq)]
+ [
struct.pack("<BhB", reminder[0], reminder[1], 1)
for reminder in reminders
]
),
timeout=GeckoConfig.PROTOCOL_TIMEOUT_IN_SECONDS,
retry_count=GeckoConfig.PROTOCOL_RETRY_COUNT,
on_retry_failed=GeckoPacketProtocolHandler.default_retry_failed_handler,
**kwargs,
)

@staticmethod
def set_ack(**kwargs: Any) -> GeckoRemindersProtocolHandler:
"""Generate a set response."""
return GeckoRemindersProtocolHandler(content=b"".join([RMSET_VERB]), **kwargs)

def __init__(self, **kwargs: Any) -> None:
"""Initialize the reminders protocol handler class."""
super().__init__(**kwargs)
self.reminders: list[tuple[GeckoReminderType, ...]] = []
self.reminders: list[tuple[GeckoReminderType, int]] = []
self.is_request: bool = False

def can_handle(self, received_bytes: bytes, _sender: tuple) -> bool:
"""Can we handle this verb."""
return received_bytes.startswith((REQRM_VERB, RMREQ_VERB))
return received_bytes.startswith(
(REQRM_VERB, RMREQ_VERB, SETRM_VERB, RMSET_VERB)
)

def _extract_reminders(self, remainder: bytes) -> None:
rest = remainder
while len(rest) > 0:
(t, days, _push, rest) = struct.unpack(f"<BhB{len(rest) - 4}s", rest)
try:
self.reminders.append((GeckoReminderType(t), days))
except ValueError:
_LOGGER.warning("Cannot use %d as reminder type, ignored", t)

def handle(self, received_bytes: bytes, _sender: tuple) -> None:
"""Handle the verb."""
remainder = received_bytes[5:]
self.is_request = False
self.reminders = []

if received_bytes.startswith(REQRM_VERB):
self._sequence = struct.unpack(">B", remainder[0:1])[0]
self.is_request = True
return # Stay in the handler list

if received_bytes.startswith(SETRM_VERB):
self._sequence = struct.unpack(">B", remainder[0:1])[0]
self._extract_reminders(remainder[1:])
return # Stay in the handler list

if received_bytes.startswith(RMSET_VERB):
pass

# Otherwise must be RMREQ
rest = remainder
while len(rest) > 0:
(t, days, _push, rest) = struct.unpack(f"<BhB{len(rest) - 4}s", rest)
try:
self.reminders.append((GeckoReminderType(t), days))
except ValueError:
_LOGGER.warning("Cannot use %d as reminder type, ignored", t)
if received_bytes.startswith(RMREQ_VERB):
self._extract_reminders(remainder)

self._should_remove_handler = True
45 changes: 27 additions & 18 deletions src/geckolib/utils/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ def __init__(self) -> None:
setattr(self._action, name, None)

self._current_watercare_mode = GeckoConstants.WATERCARE_MODE[1]
self._reminders: list[tuple[GeckoReminderType, int]] = [
(GeckoReminderType.RINSE_FILTER, -13),
(GeckoReminderType.CLEAN_FILTER, 0),
(GeckoReminderType.CHANGE_WATER, 47),
(GeckoReminderType.CHECK_SPA, 687),
(GeckoReminderType.INVALID, -13),
(GeckoReminderType.INVALID, -13),
(GeckoReminderType.INVALID, 0),
(GeckoReminderType.INVALID, 0),
(GeckoReminderType.INVALID, 0),
(GeckoReminderType.INVALID, 0),
]

async def __aenter__(self) -> Self:
"""Support async with."""
Expand Down Expand Up @@ -442,7 +454,7 @@ def _install_standard_handlers(self) -> None:
# Reminders
self.add_task(
GeckoRemindersProtocolHandler(
async_on_handled=self._async_on_get_reminders
async_on_handled=self._async_on_reminders
).consume(self._protocol),
"Reminders",
"SIM",
Expand Down Expand Up @@ -607,29 +619,26 @@ async def _async_on_watercare(
sender,
)

async def _async_on_get_reminders(
async def _async_on_reminders(
self, handler: GeckoRemindersProtocolHandler, sender: tuple
) -> None:
if self._should_ignore(handler, sender):
return
assert self._protocol is not None # noqa: S101
if handler.is_request:
self._protocol.queue_send(
GeckoRemindersProtocolHandler.req_response(
self._reminders,
parms=sender,
),
sender,
)
return

assert handler.reminders # noqa: S101
self._reminders = handler.reminders
self._protocol.queue_send(
GeckoRemindersProtocolHandler.response(
[
(GeckoReminderType.RINSE_FILTER, -13),
(GeckoReminderType.CLEAN_FILTER, 0),
(GeckoReminderType.CHANGE_WATER, 47),
(GeckoReminderType.CHECK_SPA, 687),
(GeckoReminderType.INVALID, -13),
(GeckoReminderType.INVALID, -13),
(GeckoReminderType.INVALID, 0),
(GeckoReminderType.INVALID, 0),
(GeckoReminderType.INVALID, 0),
(GeckoReminderType.INVALID, 0),
],
parms=sender,
),
sender,
GeckoRemindersProtocolHandler.set_ack(parms=sender), sender
)

async def _async_on_update_firmware(
Expand Down
Loading

0 comments on commit a2e972a

Please sign in to comment.