Skip to content

Commit

Permalink
Merge pull request #57 from gazoodle/main
Browse files Browse the repository at this point in the history
Merge main to dev
  • Loading branch information
gazoodle authored Jan 27, 2025
2 parents ccac6ad + 3656c06 commit 48646dc
Show file tree
Hide file tree
Showing 224 changed files with 141,032 additions and 173,605 deletions.
46 changes: 46 additions & 0 deletions .devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "gazoodle/geckolib",
"image": "mcr.microsoft.com/devcontainers/python:3.13",
"customizations": {
"vscode": {
"extensions": [
"charliermarsh.ruff",
"github.vscode-pull-request-github",
"ms-python.python",
"ms-python.vscode-pylance",
"ryanluker.vscode-coverage-gutters"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"files.trimTrailingWhitespace": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"python.defaultInterpreterPath": "/usr/local/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
}
}
},
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"packages": [
"ffmpeg",
"libturbojpeg0",
"libpcap-dev",
"iputils-ping",
]
}
},
"runArgs": [
"-v",
"${env:HOME}${env:USERPROFILE}/.ssh:/tmp/.ssh",
"--add-host",
"spa=10.1.209.91"
]
}
4 changes: 2 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ jobs:
python-version: ["3.8", "3.9", "3.10"]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
Expand Down
62 changes: 50 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ Home Assistant integration. This has now been released, in preview, and can be
found at https://github.com/gazoodle/gecko-home-assistant, or from HACS by adding
a new integration and seaching for Gecko_

## WARNING
This is the last version of this library that will support sync and async. The
next release will remove the sync API as it's too convoluted to support both in
the same codebase now.

# Async support

The core of the library has been rewritten to be async based. This is for several
Expand Down Expand Up @@ -293,7 +298,7 @@ Turning pump 1 off

There is also a complete async sample which can be found in the repo under
the /sample folder. This can be run using `python3 complete.py`. Full path
https://github.com/gazoodle/geckolib/tree/main/sample
https://github.com/gazoodle/geckolib/tree/main/sample. Only works on Linux

# Home Assistant integration

Expand All @@ -317,35 +322,68 @@ https://www.gnu.org/licenses/gpl-3.0.html

# Todo

- Spa state (errors)
- Error handling (ongoing)
- Pythonize where possible
- APIs to support integration into automation systems (Ongoing)
- Warnings/Errors
- Diagnostics
- More unit tests
- Handle other device types such as Waterfall
- Handle inMix for lighting control
- Add API documentation
- Merge reminders branch from @kalinrow
- List property for User demands in pack classes
- List property for errors in pack classes
- Tidy up support files. One class per file
- Full sweep for typing hints - Ongoing
- Add sensor for errors
- Add switch for winterizing
- Add ability to set hours so we can implement a crude clock sync mechanism
- Think about a way to provide access to behaviour refresh frequencies so that
it can be customised
- Look into getting shell & simulator using async API so that there are no
internal dependencies on the sync code any longer
- Move to pytest unit test framework
- Use snapshots to generate some specific tests
- Build some documentation
- Add coverage to GitHub package workflow
- There is a lock issue when a command is being retried as the protocol lock
- API set_config_mode needs to be per device rather than global

## Done/Fixed in 0.4.19

- Removed unprintable charater in RF Channel name
- Reworked packgen.py so that the code is RUFF compliant, working towards getting Github actions working
- Fixed simulator to load new HA snapshots
- Significant work on refactoring now with better async understanding
- Lots of RUFF updates
- Removed weird sleep constant that was the root of some CPU usaghe issues, hopefully this has
gone now we're using a better async pattern.
- Shell and Simulator moving to async pattern in prep for deleting the sync API.
- Big refactor to help track down some obscure bugs, including a rare and hard to reproduce deadlock
- More robust pattern for transport socket usage hopefully clean-up potential
leaking socket.
- Fixed lock issue when a command is being retried as the protocol lock
is busy and the CUI won't exit until the timeout has been reached (this can
be reproduced by making the simulator stop responding to watercare requests)
- Marked all sync APIs as deprecated
- Push this code to github and release it as it will be the last version that
supports the sync API. It's getting too cluttered to maintain sync and async
in the same codebase.

## Done/Fixed in 0.4.18

- Actually increment the version number and push to GIT before publishing. I might get the hang of
this one day :-)

## Done/Fixed in 0.4.17

- Expose water heater internal sensors so they can be exposed in the home assistant integration

## Done/Fixed in 0.4.16

- Change min temperature from 15C to 8C
- Handle unnamed SPA (Issue #54)
- Prevent watercare from being out-of-range at the expense of knowing if it was ... it's more stable for users (#40)
- Fixed async importlib code from blocking by using loop executor

## Done/Fixed in 0.4.15

- Merged latest SpaPackStruct data from BenSeverson

## Done/Fixed in 0.4.9

- Merged pull to reduce asyncio sleep timeout to reduce processor usage

## Done/Fixed in 0.4.8

Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pip>=21.3.1
ruff==0.9.1
requests==2.32.3
26 changes: 26 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml

target-version = "py312"

[lint]
select = [
"ALL",
]

ignore = [
"ANN101", # Missing type annotation for `self` in method
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
"D203", # no-blank-line-before-class (incompatible with formatter)
"D212", # multi-line-summary-first-line (incompatible with formatter)
"COM812", # incompatible with formatter
"ISC001", # incompatible with formatter
]

[lint.flake8-pytest-style]
fixture-parentheses = false

[lint.pyupgrade]
keep-runtime-typing = true

[lint.mccabe]
max-complexity = 25
77 changes: 58 additions & 19 deletions sample/abstract_display.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,80 @@
""" Abstract curses display class for use in asyncio app - Thanks
to https://gist.github.com/davesteele/8838f03e0594ef11c89f77a7bca91206 """
"""
Abstract curses display class for use in asyncio app.
Thanks to https://gist.github.com/davesteele/8838f03e0594ef11c89f77a7bca91206
"""

import _curses
import asyncio
import logging
from abc import ABC, abstractmethod
from curses import ERR, KEY_RESIZE, curs_set
from context_sample import GeckoConstants # type: ignore

import _curses
from context_sample import GeckoAsyncTaskMan

_LOGGER = logging.getLogger(__name__)


class AbstractDisplay(ABC):
def __init__(self, stdscr: "_curses._CursesWindow"):
"""Abstract display class."""

def __init__(self, stdscr: _curses.window) -> None:
"""Initialize the class."""
self.stdscr = stdscr
self.done: bool = False
self.done_event = asyncio.Event()
self.queue = asyncio.Queue(5)

@abstractmethod
def make_display(self) -> None:
pass
"""Make a display."""

@abstractmethod
async def handle_char(self, char: int) -> None:
pass
"""Handle a character."""

def set_exit(self) -> None:
self.done = True
"""Indicagte we can exit."""
self.done_event.set()

async def enqueue_input(self) -> None:
"""Get input and queue it up."""
while not self.done_event.is_set():
char = self.stdscr.getch()
await self.queue.put(char)

async def process_input(self) -> None:
"""Get queue data and process it."""
try:
while not self.done_event.is_set():
char = await self.queue.get()
if char == ERR:
# Do nothing and let the loop continue without sleeping continue
pass
elif char == KEY_RESIZE:
self.make_display()
else:
await self.handle_char(char)
self.queue.task_done()

except asyncio.CancelledError:
_LOGGER.debug("Input loop cancelled")
raise

async def run(self) -> None:
except: # noqa
_LOGGER.exception("Exception in input loop")
raise

finally:
_LOGGER.debug("Input loop finished")

async def run(self, taskman: GeckoAsyncTaskMan) -> None:
"""Run the display class."""
curs_set(0)
self.stdscr.nodelay(True)
self.stdscr.nodelay(True) # noqa: FBT003

self.make_display()

while not self.done:
char = self.stdscr.getch()
if char == ERR:
await asyncio.sleep(GeckoConstants.ASYNCIO_SLEEP_TIMEOUT_FOR_YIELD)
elif char == KEY_RESIZE:
self.make_display()
else:
await self.handle_char(char)
taskman.add_task(self.enqueue_input(), "Input gather", "CUI")
taskman.add_task(self.process_input(), "Process input", "CUI")
await self.done_event.wait()
taskman.cancel_key_tasks("CUI")
58 changes: 29 additions & 29 deletions sample/complete.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
#!/usr/bin/python3.9
""" Complete sample client
This sample is built as a complete async client demonstrating all
the code that might be needed in a full client of the library,
e.g. in a home automation system.
"""
Complete sample client.
It was used to help develop the change from sync to async as
this seems to be where all the HA integrations are going anyway
This sample is built as a complete async client demonstrating all
the code that might be needed in a full client of the library,
e.g. in a home automation system.
I would have loved to have one library that could do both, but
this was increasingly difficult to acheive and I was spending
quite a bit of time in thread faff which is something that I'm
sure is not needed in the async world ... time will tell
It was used to help develop the change from sync to async as
this seems to be where all the HA integrations are going anyway
I would have loved to have one library that could do both, but
this was increasingly difficult to acheive and I was spending
quite a bit of time in thread faff which is something that I'm
sure is not needed in the async world ... time will tell.
"""
import os
import logging

import asyncio
import curses
import logging
from pathlib import Path

from cui import CUI
from curses import wrapper
from context_sample import GeckoConstants # type: ignore


_LOGGER = logging.getLogger(__name__)


def install_logging():
"""Everyone needs logging, you say when, you say where, you say how much"""
os.remove("cui.log")
def install_logging() -> None:
"""Everyone needs logging, you say when, you say where, you say how much."""
Path("cui.log").unlink(True)
file_logger = logging.FileHandler("cui.log")
file_logger.setLevel(logging.DEBUG)
file_logger.setFormatter(
Expand All @@ -38,20 +34,24 @@ def install_logging():
logging.getLogger().setLevel(logging.DEBUG)


async def async_main(stdscr):
"""Async main manages the console UI"""
async def async_main(stdscr: curses.window) -> None:
"""Async main manages the console UI."""
task = asyncio.current_task()
task.set_name("CUI main")
async with CUI(stdscr):
await asyncio.sleep(GeckoConstants.ASYNCIO_SLEEP_TIMEOUT_FOR_YIELD)
pass


def main(stdscr):
"""This main installs logging and then runs the async loop"""
def main(stdscr: curses.window) -> None:
"""Install logging and then runs the async loop."""
install_logging()
asyncio.run(async_main(stdscr))
asyncio.run(
async_main(stdscr),
)


########################################################################################
#
# Entry point
if __name__ == "__main__":
wrapper(main)
curses.wrapper(main)
Loading

0 comments on commit 48646dc

Please sign in to comment.