From a2e972aecfba2626eab4553d503df8811301ed0d Mon Sep 17 00:00:00 2001 From: Gazoodle Date: Mon, 17 Feb 2025 19:30:00 +0000 Subject: [PATCH] Support setting remaining days on reminders --- README.md | 1 + src/geckolib/async_spa.py | 24 +++++ src/geckolib/automation/reminders.py | 67 ++++++-------- src/geckolib/driver/protocol/reminders.py | 69 +++++++++++--- src/geckolib/utils/simulator.py | 45 +++++---- tests/test_protocol_reminders.py | 106 +++++++++++++++++++++- 6 files changed, 243 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 997f74a..c278eda 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/geckolib/async_spa.py b/src/geckolib/async_spa.py index cc3b52c..e8dd584 100644 --- a/src/geckolib/async_spa.py +++ b/src/geckolib/async_spa.py @@ -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 @@ -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() diff --git a/src/geckolib/automation/reminders.py b/src/geckolib/automation/reminders.py index 439a900..3ad901d 100644 --- a/src/geckolib/automation/reminders.py +++ b/src/geckolib/automation/reminders.py @@ -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 @@ -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.""" @@ -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.""" @@ -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: diff --git a/src/geckolib/driver/protocol/reminders.py b/src/geckolib/driver/protocol/reminders.py index 2ea0674..e96a6e2 100644 --- a/src/geckolib/driver/protocol/reminders.py +++ b/src/geckolib/driver/protocol/reminders.py @@ -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" @@ -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.""" @@ -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(" 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" 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" 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.""" @@ -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", @@ -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( diff --git a/tests/test_protocol_reminders.py b/tests/test_protocol_reminders.py index e424cc1..fea0b9c 100644 --- a/tests/test_protocol_reminders.py +++ b/tests/test_protocol_reminders.py @@ -25,7 +25,7 @@ def test_send_construct_request(self) -> None: ) def test_send_construct_response(self) -> None: - handler = GeckoRemindersProtocolHandler.response( + handler = GeckoRemindersProtocolHandler.req_response( [ (GeckoReminderType.RINSE_FILTER, -13), (GeckoReminderType.CLEAN_FILTER, 257), @@ -51,10 +51,48 @@ def test_send_construct_response(self) -> None: b"", ) + def test_send_construct_set(self) -> None: + handler = GeckoRemindersProtocolHandler.set( + 1, + [ + (GeckoReminderType.RINSE_FILTER, -13), + (GeckoReminderType.CLEAN_FILTER, 257), + (GeckoReminderType.CHANGE_WATER, 2), + (GeckoReminderType.CHECK_SPA, 512), + (GeckoReminderType.CHANGE_OZONATOR, 0), + (GeckoReminderType.CHANGE_VISION_CARTRIDGE, 128), + (GeckoReminderType.INVALID, 1), + ], + parms=PARMS, + ) + self.assertEqual( + handler.send_bytes, + b"DESTIDSRCID" + b"SETRM\x01" + b"\x01\xf3\xff\x01" + b"\x02\x01\x01\x01" + b"\x03\x02\x00\x01" + b"\x04\x00\x02\x01" + b"\x05\x00\x00\x01" + b"\x06\x80\x00\x01" + b"\x00\x01\x00\x01" + b"", + ) + + def test_send_construct_set_ack(self) -> None: + handler = GeckoRemindersProtocolHandler.set_ack(parms=PARMS) + self.assertEqual( + handler.send_bytes, + b"DESTIDSRCID" + b"RMSET", + ) + def test_recv_can_handle(self) -> None: handler = GeckoRemindersProtocolHandler() self.assertTrue(handler.can_handle(b"REQRM", PARMS)) self.assertTrue(handler.can_handle(b"RMREQ", PARMS)) + self.assertTrue(handler.can_handle(b"SETRM", PARMS)) + self.assertTrue(handler.can_handle(b"RMSET", PARMS)) self.assertFalse(handler.can_handle(b"OTHER", PARMS)) def test_recv_handle_request(self) -> None: @@ -141,6 +179,72 @@ def test_recv_handle_negative_number_response(self) -> None: ], ) + def test_recv_handle_set_reminder(self) -> None: + handler = GeckoRemindersProtocolHandler() + handler.handle( + b"SETRM\x16\x01\x1e\x00\x01\x02\x00\x00\x01\x03/\x00\x01\x04\xaf\x02\x01\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00", + PARMS, + ) + self.assertListEqual( + handler.reminders, + [ + (GeckoReminderType.RINSE_FILTER, 30), + (GeckoReminderType.CLEAN_FILTER, 0), + (GeckoReminderType.CHANGE_WATER, 47), + (GeckoReminderType.CHECK_SPA, 687), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + ], + ) + + def test_recv_handle_set_reminder_reset_all(self) -> None: + handler = GeckoRemindersProtocolHandler() + handler.handle( + b"SETRMA\x01\x1e\x00\x01\x02<\x00\x01\x03Z\x00\x01\x04\xda\x02\x01\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00", + PARMS, + ) + self.assertListEqual( + handler.reminders, + [ + (GeckoReminderType.RINSE_FILTER, 30), + (GeckoReminderType.CLEAN_FILTER, 60), + (GeckoReminderType.CHANGE_WATER, 90), + (GeckoReminderType.CHECK_SPA, 730), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + (GeckoReminderType.INVALID, 30), + ], + ) + + def test_recv_handle_set_reminder_reset_sim(self) -> None: + handler = GeckoRemindersProtocolHandler() + handler.handle( + b"SETRM\t\x01\xf3\xff\x01\x02\x00\x00\x01\x03\x00\x00\x01\x04\xaf\x02\x01\x00\xf3\xff\x01\x00\xf3\xff\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01", + PARMS, + ) + self.assertListEqual( + handler.reminders, + [ + (GeckoReminderType.RINSE_FILTER, -13), + (GeckoReminderType.CLEAN_FILTER, 0), + (GeckoReminderType.CHANGE_WATER, 0), + (GeckoReminderType.CHECK_SPA, 687), + (GeckoReminderType.INVALID, -13), + (GeckoReminderType.INVALID, -13), + (GeckoReminderType.INVALID, 0), + (GeckoReminderType.INVALID, 0), + (GeckoReminderType.INVALID, 0), + (GeckoReminderType.INVALID, 0), + ], + ) + if __name__ == "__main__": unittest.main()