Skip to content

Commit

Permalink
Rapid spa connection now by avoiding a double locate
Browse files Browse the repository at this point in the history
Tidy through shell commands
  • Loading branch information
gazoodle committed Jan 29, 2025
1 parent 109a12a commit 116fe92
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 74 deletions.
13 changes: 12 additions & 1 deletion src/geckolib/async_spa_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,18 @@ async def _sequence_pump(self) -> None:
and self._facade is None
):
_LOGGER.debug("Sequence pump did connect")
await self.async_connect(self._spa_identifier, self._spa_address)
if self._spa_descriptors is not None:
await self.async_connect_to_spa(
next(
d
for d in self._spa_descriptors
if d.identifier_as_string == self._spa_identifier
)
)
else:
await self.async_connect(
self._spa_identifier, self._spa_address
)

except asyncio.CancelledError:
_LOGGER.debug("Spaman sequence pump cancelled")
Expand Down
13 changes: 9 additions & 4 deletions src/geckolib/async_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import asyncio
import logging
from typing import Self
from typing import Coroutine, Self

from .config import GeckoConfig, config_sleep

Expand All @@ -22,7 +22,7 @@ class AsyncTasks:

def __init__(self) -> None:
"""Initialize async tasks class."""
self._tasks = []
self._tasks: list[asyncio.Task] = []

async def __aenter__(self) -> Self:
"""Async enter, used from python's with statement."""
Expand All @@ -34,10 +34,15 @@ async def __aexit__(self, *_exc_info: object) -> None:
"""Async exit, when out of scope."""
await self.gather()

def add_task(self, coroutine, name_: str, key_: str) -> None:
def add_task(self, coroutine: Coroutine, name_: str, key_: str) -> None:
"""Add tasks to the task list."""
taskname = f"{key_}:{name_}"
for task in self._tasks:
if not task.done() and task.get_name() == taskname:
msg = f"Task {taskname} already running"
raise RuntimeError(msg)
_LOGGER.debug("Starting task `%s` in domain `%s`", name_, key_)
task = asyncio.create_task(coroutine, name=f"{key_}:{name_}")
task = asyncio.create_task(coroutine, name=taskname)
self._tasks.append(task)

def cancel_key_tasks(self, key_: str) -> None:
Expand Down
144 changes: 76 additions & 68 deletions src/geckolib/utils/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import datetime
import logging
import sys
from time import sleep
import traceback
from typing import Self

from geckolib import VERSION, GeckoConstants, GeckoPump
from geckolib.async_locator import GeckoAsyncLocator
from geckolib.async_spa_manager import GeckoAsyncSpaMan
from geckolib.config import config_sleep
from geckolib.spa_events import GeckoSpaEvent
from geckolib.spa_state import GeckoSpaState

from .shared_command import GeckoCmd

Expand Down Expand Up @@ -45,59 +48,99 @@ def __init__(self) -> None:
GeckoAsyncSpaMan.__init__(self, SHELL_UUID)
GeckoCmd.__init__(self, self)

# Fill the watercare mode strings in.
self.do_watercare.__func__.__doc__ = self.do_watercare.__doc__.format(
GeckoConstants.WATERCARE_MODE_STRING
)

self.intro = "Welcome to the Gecko shell. Type help or ? to list commands.\n"
self.prompt = "(Gecko) "
self.push_command("discover")

async def __aenter__(self) -> Self:
_LOGGER.info("Async enter called on %s", self.__class__.__name__)
await GeckoAsyncSpaMan.__aenter__(self)
return self

async def handle_event(self, event: GeckoSpaEvent, **_kwargs: object) -> None:
"""Handle the event."""
_LOGGER.debug("Handle event %s", event)
# print(event)
if event == GeckoSpaEvent.CLIENT_FACADE_IS_READY:
if self._spa is not None:
self.structure = self._spa.struct

elif event == GeckoSpaEvent.CLIENT_FACADE_TEARDOWN:
self.structure = None

async def do_discover(self, arg) -> None:
elif event == GeckoSpaEvent.LOCATING_STARTED:
print("Starting discovery process...", end="", flush=True)

elif event == GeckoSpaEvent.LOCATING_FINISHED:
self.spas = self._spa_descriptors
number_of_spas = len(self.spas)
print(f"Found {number_of_spas} spas", flush=True)
if number_of_spas == 0:
_LOGGER.warning(
"Try using the iOS or Android app to confirm they are "
"functioning correctly"
)
sys.exit(1)
if number_of_spas == 1:
self.do_manage("1")
else:
print(self.prompt, end="", flush=True)

elif event == GeckoSpaEvent.CONNECTION_FINISHED:
assert self.facade is not None
print(f"connected to {self.facade.name}!", flush=True)
self.prompt = f"{self.facade.name}$ "

# Build list of spa commands
for device in self.facade.all_user_devices:
if isinstance(device, GeckoPump):
func_name = f"do_{device.ui_key}"

async def async_pump_command(self, arg, device=device):
return await self.pump_command(arg, device)

setattr(
GeckoShell,
func_name,
async_pump_command,
)
func_ptr = getattr(GeckoShell, func_name)
func_ptr.__doc__ = (
f"Set pump {device.name} mode: {device.ui_key} <OFF|LO|HI>"
)
else:
func_name = f"do_{device.ui_key}"
setattr(
GeckoShell,
func_name,
lambda self, arg, device=device: self.device_command(
arg, device
),
)
func_ptr = getattr(GeckoShell, func_name)
func_ptr.__doc__ = (
f"Turn device {device.name} ON or OFF: {device.ui_key} <ON|OFF>"
)

self.do_state("")
print(self.prompt, end="", flush=True)

def do_discover(self, arg: str) -> None:
"""Discover all the in.touch2 devices on your network : discover [<ip address>]."""
if self.spas is not None:
return

print(
"Starting discovery process...",
end="",
flush=True,
)

locator = GeckoAsyncLocator(
self,
self._handle_event,
spa_address=arg,
)
await locator.discover()
self.spas = locator.spas

number_of_spas = len(self.spas)
print(f"Found {number_of_spas} spas")
if number_of_spas == 0:
_LOGGER.warning(
"Try using the iOS or Android app to confirm they are "
"functioning correctly"
)
sys.exit(1)
if number_of_spas == 1:
self.push_command("manage 1")
self._spa_address = arg

def do_list(self, _arg) -> None:
"""List the spas that are available to manage : list."""
for idx, spa in enumerate(self.spas):
print(f"{idx + 1}. {spa.name}")
print(f"{idx + 1}. {spa.name} ({spa.ipaddress})")

async def do_manage(self, arg) -> None:
def do_manage(self, arg) -> None:
"""Manage a named or numbered spa : manage 1."""
spa_to_manage = int(arg)
spa_descriptor = self.spas[spa_to_manage - 1]
Expand All @@ -107,43 +150,8 @@ async def do_manage(self, arg) -> None:
flush=True,
)
_LOGGER.debug(spa_descriptor)

await self.async_connect_to_spa(spa_descriptor)
await self.wait_for_facade()
await self.facade.wait_for_one_update()
print(f"connected to {self.facade.name}!")
self.prompt = f"{self.facade.name}$ "

# Build list of spa commands
for device in self.facade.all_user_devices:
if isinstance(device, GeckoPump):
func_name = f"do_{device.ui_key}"

async def async_pump_command(self, arg, device=device):
return await self.pump_command(arg, device)

setattr(
GeckoShell,
func_name,
async_pump_command,
)
func_ptr = getattr(GeckoShell, func_name)
func_ptr.__doc__ = (
f"Set pump {device.name} mode: {device.ui_key} <OFF|LO|HI>"
)
else:
func_name = f"do_{device.ui_key}"
setattr(
GeckoShell,
func_name,
lambda self, arg, device=device: self.device_command(arg, device),
)
func_ptr = getattr(GeckoShell, func_name)
func_ptr.__doc__ = (
f"Turn device {device.name} ON or OFF: {device.ui_key} <ON|OFF>"
)

self.push_command("state")
self._spa_identifier = spa_descriptor.identifier_as_string
self._state_change.set()

def device_command(self, arg, device):
"""Turn a device on or off."""
Expand Down Expand Up @@ -210,7 +218,7 @@ def monitor_compare_states(self, states):
def monitor_print_states(self, states):
print(f"{datetime.datetime.now()} : {' '.join(states)}")

async def do_monitor(self, _arg):
def do_monitor(self, _arg):
"""Monitor the state of the managed spa outputting a new line for each change : monitor"""
if self.facade is None:
print("Must be connected to a spa")
Expand All @@ -223,7 +231,7 @@ async def do_monitor(self, _arg):
if self.monitor_compare_states(current_state):
current_state = self.monitor_get_states()
self.monitor_print_states(current_state)
await config_sleep(1, "Shell monitor loop")
sleep(1)
except KeyboardInterrupt:
print()
print("Monitor stopped")
Expand Down
4 changes: 4 additions & 0 deletions src/geckolib/utils/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import random
import socket
import struct
from typing import Self

from geckolib import VERSION
from geckolib.async_taskman import GeckoAsyncTaskMan
Expand Down Expand Up @@ -84,6 +85,9 @@ def __init__(self) -> None:
except ImportError:
pass

async def __aenter__(self) -> Self:
await GeckoAsyncTaskMan.__aenter__(self)

async def __aexit__(self, *_args: object) -> None:
"""Support 'with' statements."""
await self.do_stop("")
Expand Down
2 changes: 1 addition & 1 deletion tests/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

def install_logging() -> None:
"""Everyone needs logging, you say when, you say where, you say how much."""
Path("client.log").unlink(True)
Path("client.log").unlink(True) # noqa: FBT003
file_logger = logging.FileHandler("client.log")
file_logger.setLevel(logging.DEBUG)
file_logger.setFormatter(
Expand Down

0 comments on commit 116fe92

Please sign in to comment.