Skip to content

Commit

Permalink
Active & Idle configuration settings added
Browse files Browse the repository at this point in the history
Facade controls which config mode to use
  • Loading branch information
gazoodle committed Mar 23, 2022
1 parent f0dcd4e commit 77cfaa7
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 52 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,16 @@ https://www.gnu.org/licenses/gpl-3.0.html

## Done/Fixed in 0.4.4

- Moved config settings out of const class into their own class
- Added idle/active config settings and task loop for library to switch between
idle and active config settings. An idle spa is one that is currently not processing
any user demands, an active spa is one that has received a client request such as to
change temp or turn on a pump. Active mode will stay on while any user demand is
currently live.
- Replaced asyncio.sleep in various places with config_sleep which is aware of when
the configuration values have changed which means that the loops currently waiting on
these values stop waiting and can collect the new values.

## Done/Fixed in 0.4.2

- Fixed processor getting pegged at 100% but not using asyncio.sleep(0)
Expand Down
25 changes: 20 additions & 5 deletions sample/cui.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
GeckoSpaEvent,
GeckoAsyncSpaDescriptor,
GeckoConstants,
GeckoConfig,
)
from typing import Optional

Expand Down Expand Up @@ -189,11 +190,25 @@ def make_display(self) -> None:
lines.append("")

if self._can_use_facade:
lines.append("Press 'b' to toggle blower")
if self.facade.blowers[0].is_on:
self._commands["b"] = self.facade.blowers[0].async_turn_off
else:
self._commands["b"] = self.facade.blowers[0].async_turn_on
assert self.facade is not None
if self.facade.blowers:
lines.append("Press 'b' to toggle blower")
if self.facade.blowers[0].is_on:
self._commands["b"] = self.facade.blowers[0].async_turn_off
else:
self._commands["b"] = self.facade.blowers[0].async_turn_on
if self.facade.pumps:
lines.append("Press 'p' to toggle pump 1")
if self.facade.pumps[0].mode == "OFF":
self._commands["p"] = (
self.facade.pumps[0].async_set_mode,
"HI",
)
else:
self._commands["p"] = (
self.facade.pumps[0].async_set_mode,
"OFF",
)

lines.append("Press '+' to increase setpoint")
self._commands["+"] = self.increase_temp
Expand Down
3 changes: 3 additions & 0 deletions src/geckolib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .async_tasks import AsyncTasks
from .const import GeckoConstants
from .config import GeckoConfig
from .automation import (
GeckoAutomationBase,
GeckoAutomationFacadeBase,
Expand Down Expand Up @@ -84,6 +85,8 @@
"GeckoReminders",
# From constants
"GeckoConstants",
# From config
"GeckoConfig",
# From facade
"GeckoFacade",
# From locator
Expand Down
6 changes: 3 additions & 3 deletions src/geckolib/async_spa.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .spa_events import GeckoSpaEvent

from .const import GeckoConstants
from .config import GeckoConfig
from .config import GeckoConfig, config_sleep
from .driver import (
GeckoAsyncUdpProtocol,
GeckoPacketProtocolHandler,
Expand Down Expand Up @@ -448,7 +448,7 @@ async def _ping_loop(self) -> None:
)

# Ping every couple of seconds until we have had a response
await asyncio.sleep(
await config_sleep(
2
if self._last_ping_at is None
else GeckoConfig.PING_FREQUENCY_IN_SECONDS
Expand Down Expand Up @@ -479,7 +479,7 @@ async def _refresh_loop(self) -> None:

_LOGGER.debug("Refresh loop started")
while self.isopen:
await asyncio.sleep(GeckoConfig.SPA_PACK_REFRESH_FREQUENCY_IN_SECONDS)
await config_sleep(GeckoConfig.SPA_PACK_REFRESH_FREQUENCY_IN_SECONDS)
if not self.is_connected:
continue
if not self.is_responding_to_pings:
Expand Down
4 changes: 2 additions & 2 deletions src/geckolib/async_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging
import asyncio
from .config import GeckoConfig
from .config import GeckoConfig, config_sleep

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -41,7 +41,7 @@ async def gather(self) -> None:
async def _tidy(self) -> None:
try:
while True:
await asyncio.sleep(GeckoConfig.TASK_TIDY_FREQUENCY_IN_SECONDS)
await config_sleep(GeckoConfig.TASK_TIDY_FREQUENCY_IN_SECONDS)
if _LOGGER.isEnabledFor(logging.DEBUG):
for task in self._tasks:
if task.done():
Expand Down
22 changes: 19 additions & 3 deletions src/geckolib/automation/async_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from .blower import GeckoBlower
from ..const import GeckoConstants
from ..config import GeckoConfig
from ..config import GeckoConfig, config_sleep, set_config_mode
from .heater import GeckoWaterHeater
from .keypad import GeckoKeypad
from .light import GeckoLight
Expand Down Expand Up @@ -58,10 +58,20 @@ def __init__(self, spa: GeckoAsyncSpa, taskman: AsyncTasks, **kwargs: str) -> No
# Install change notifications
for device in self.all_automation_devices:
device.watch(self._on_change)
# And notifications for active/idle items
for device in self.all_config_change_devices:
device.watch(self._on_config_device_change)

self._taskman.add_task(self._facade_update(), "Facade update", "FACADE")
self._ready = False

def _on_config_device_change(self, *args) -> None:
active_mode = False
for device in self.all_config_change_devices:
if device.is_on: # type: ignore
active_mode = True
set_config_mode(active_mode)

async def _facade_update(self) -> None:
_LOGGER.debug("Facade update task started")
try:
Expand All @@ -77,6 +87,7 @@ async def _facade_update(self) -> None:
self._reminders_manager.change_reminders(
await self._spa.async_get_reminders()
)
self._on_config_device_change()

# After we've been round here at least once, we're ready
self._ready = True
Expand All @@ -87,7 +98,7 @@ async def _facade_update(self) -> None:
if self._spa.is_responding_to_pings
else GeckoConfig.PING_FREQUENCY_IN_SECONDS
)
await asyncio.sleep(wait_time)
await config_sleep(wait_time)

except asyncio.CancelledError:
_LOGGER.debug("Facade update loop cancelled")
Expand Down Expand Up @@ -282,7 +293,12 @@ def eco_mode(self) -> Optional[GeckoSwitch]:
@property
def all_user_devices(self) -> List[GeckoAutomationBase]:
"""Get all the user controllable devices as a list"""
return self._pumps + self._blowers + self._lights # type:ignore
return self._pumps + self._blowers + self._lights # type: ignore

@property
def all_config_change_devices(self) -> List[GeckoAutomationBase]:
"""Get all devices that can cause config change"""
return self._pumps + self._blowers # type: ignore

@property
def all_automation_devices(self) -> List[GeckoAutomationBase]:
Expand Down
18 changes: 13 additions & 5 deletions src/geckolib/automation/pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import logging

from ..const import GeckoConstants
from .base import GeckoAutomationFacadeBase
from .sensors import GeckoSensor

logger = logging.getLogger(__name__)
_LOGGER = logging.getLogger(__name__)


class GeckoPump(GeckoAutomationFacadeBase):
Expand All @@ -23,6 +24,13 @@ def __init__(self, facade, key, props, user_demand):
self.device_class = props[3]
self._user_demand = user_demand

@property
def is_on(self):
"""True if the device is running, False otherwise"""
if self._state_sensor.accessor.type == GeckoConstants.SPA_PACK_STRUCT_BOOL_TYPE:
return self._state_sensor.state
return self._state_sensor.state != "OFF"

@property
def modes(self):
return self._user_demand["options"]
Expand All @@ -33,21 +41,21 @@ def mode(self):

def set_mode(self, mode):
try:
logger.debug("%s set mode %s", self.name, mode)
_LOGGER.debug("%s set mode %s", self.name, mode)
self.facade.spa.accessors[self._user_demand["demand"]].value = mode
except Exception: # pylint: disable=broad-except
logger.exception(
_LOGGER.exception(
"Exception handling setting %s=%s", self._user_demand["demand"], mode
)

async def async_set_mode(self, mode):
try:
logger.debug("%s async set mode %s", self.name, mode)
_LOGGER.debug("%s async set mode %s", self.name, mode)
await self.facade.spa.accessors[
self._user_demand["demand"]
].async_set_value(mode)
except Exception: # pylint: disable=broad-except
logger.exception(
_LOGGER.exception(
"Exception handling setting %s=%s", self._user_demand["demand"], mode
)

Expand Down
108 changes: 75 additions & 33 deletions src/geckolib/config.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,88 @@
"""Configuration management for geckolib"""

import asyncio
from dataclasses import dataclass
import logging
from typing import Optional

class _GeckoConfigManager:
"""Configuration manangement class"""
_LOGGER = logging.getLogger(__name__)

@property
def DISCOVERY_INITIAL_TIMEOUT_IN_SECONDS(self):
"""Mininum time in seconds to wait for initial spa discovery even
if one spa has responded"""
return 4

@property
def DISCOVERY_TIMEOUT_IN_SECONDS(self):
"""Maximum time in seconds to wait for full discovery if no spas
have responded"""
return 10
@dataclass
class _GeckoConfig:
DISCOVERY_INITIAL_TIMEOUT_IN_SECONDS = 1
"""Mininum time in seconds to wait for initial spa discovery even
if one spa has responded"""

@property
def TASK_TIDY_FREQUENCY_IN_SECONDS(self):
"""Time in seconds between task tidyup checks"""
return 5
DISCOVERY_TIMEOUT_IN_SECONDS = 1
"""Maximum time in seconds to wait for full discovery if no spas
have responded"""

@property
def PING_FREQUENCY_IN_SECONDS(self):
"""Frequency in seconds to ping the spa to ensure it is still available"""
return 10
TASK_TIDY_FREQUENCY_IN_SECONDS = 1
"""Time in seconds between task tidyup checks"""

@property
def PING_DEVICE_NOT_RESPONDING_TIMEOUT_IN_SECONDS(self):
"""Time after which a spa is deemed to be not responding to pings"""
return 10
PING_FREQUENCY_IN_SECONDS = 1
"""Frequency in seconds to ping the spa to ensure it is still available"""

@property
def FACADE_UPDATE_FREQUENCY_IN_SECONDS(self):
"""Frequency in seconds to update facade data"""
return 30
PING_DEVICE_NOT_RESPONDING_TIMEOUT_IN_SECONDS = 1
"""Time after which a spa is deemed to be not responding to pings"""

@property
def SPA_PACK_REFRESH_FREQUENCY_IN_SECONDS(self):
"""Frequency in seconds to request all LOG data from spa"""
return 30
FACADE_UPDATE_FREQUENCY_IN_SECONDS = 1
"""Frequency in seconds to update facade data"""

SPA_PACK_REFRESH_FREQUENCY_IN_SECONDS = 1
"""Frequency in seconds to request all LOG data from spa"""


CONFIG_MEMBERS = [
attr
for attr in dir(_GeckoConfig)
if not callable(getattr(_GeckoConfig, attr)) and not attr.startswith("__")
]


@dataclass
class _GeckoActiveConfig(_GeckoConfig):
"""Gecko active configuration"""

DISCOVERY_INITIAL_TIMEOUT_IN_SECONDS = 4
DISCOVERY_TIMEOUT_IN_SECONDS = 10
TASK_TIDY_FREQUENCY_IN_SECONDS = 5
PING_FREQUENCY_IN_SECONDS = 2
PING_DEVICE_NOT_RESPONDING_TIMEOUT_IN_SECONDS = 10
FACADE_UPDATE_FREQUENCY_IN_SECONDS = 30
SPA_PACK_REFRESH_FREQUENCY_IN_SECONDS = 30


@dataclass
class _GeckoIdleConfig(_GeckoConfig):
"""Gecko idle configuration"""

DISCOVERY_INITIAL_TIMEOUT_IN_SECONDS = 4
DISCOVERY_TIMEOUT_IN_SECONDS = 10
TASK_TIDY_FREQUENCY_IN_SECONDS = 60
PING_FREQUENCY_IN_SECONDS = 60
PING_DEVICE_NOT_RESPONDING_TIMEOUT_IN_SECONDS = 120
FACADE_UPDATE_FREQUENCY_IN_SECONDS = 120
SPA_PACK_REFRESH_FREQUENCY_IN_SECONDS = 120


# Root config
GeckoConfig = _GeckoConfigManager()
GeckoConfig: _GeckoConfig = _GeckoIdleConfig()
ConfigChange: Optional[asyncio.Future] = None


def set_config_mode(active: bool) -> None:
"""Set config mode to active (true) or idle (false)."""
new_config = _GeckoActiveConfig() if active else _GeckoIdleConfig()
for member in CONFIG_MEMBERS:
setattr(GeckoConfig, member, getattr(new_config, member))
assert ConfigChange is not None
ConfigChange.set_result(True)


async def config_sleep(delay: float) -> None:
global ConfigChange
if ConfigChange is None or ConfigChange.done():
ConfigChange = asyncio.get_running_loop().create_future()
await asyncio.wait([ConfigChange], timeout=delay)
2 changes: 1 addition & 1 deletion src/geckolib/spa.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ def _ping_thread_func(self):
self.wait(GeckoConfig.PING_FREQUENCY_IN_SECONDS)
if (
time.monotonic() - self._last_ping
> GeckoConfig.PING_DEVICE_NOT_RESPONDING_TIMEOUT
> GeckoConfig.PING_DEVICE_NOT_RESPONDING_TIMEOUT_IN_SECONDS
):
logger.warning(
# TODO
Expand Down

0 comments on commit 77cfaa7

Please sign in to comment.