Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge main to dev #57

Merged
merged 57 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
3b0e1e6
update to v39 spapack config
BenSeverson May 26, 2024
1ceffdd
setup.cfg fix
BenSeverson May 29, 2024
8c137d9
Reduce asyncio sleep timeout
xela1 Jun 5, 2024
f823450
fix python_requires in setup.cfg
BenSeverson Jun 21, 2024
8b861c8
Merge pull request #48 from xela1/patch-1
gazoodle Jun 23, 2024
b407fe2
Bump version
gazoodle Jun 23, 2024
e072f1a
Update README.md
gazoodle Jun 23, 2024
29ac77a
update readme and version to 0.4.9
BenSeverson Jun 23, 2024
219b82d
Revert "update readme and version to 0.4.9"
BenSeverson Jun 23, 2024
320e097
Update python-publish.yml
gazoodle Jun 24, 2024
a8e6630
Update python-package.yml
gazoodle Jun 24, 2024
4e100bb
Update setup.cfg
gazoodle Jun 24, 2024
eb84557
Update setup.cfg
gazoodle Jun 24, 2024
933a756
Update setup.cfg
gazoodle Jun 24, 2024
fd2355c
Merge pull request #49 from BenSeverson/spapackv39
gazoodle Jun 24, 2024
e78e709
Update README.md
gazoodle Jun 24, 2024
79b0ef8
Update _version.py
gazoodle Jun 24, 2024
4bc81d7
Add DS_STore to .gitignore
gazoodle Jan 13, 2025
f235c7a
Bump version
gazoodle Jan 13, 2025
caedfb1
Prevent watercare from being out-of-range
gazoodle Jan 13, 2025
5d4f2b5
Handle unnamed spa #54
gazoodle Jan 13, 2025
af4b814
Reduce min temperature to 8C from 15C
gazoodle Jan 13, 2025
435e198
Updare README
gazoodle Jan 13, 2025
666f341
Expose the water heater internal sensor objects so they can be added …
gazoodle Jan 15, 2025
79f9042
Bump version
gazoodle Jan 15, 2025
c640912
Bump version, update README
gazoodle Jan 15, 2025
66850c2
Unprintable charater in RF Channel name
gazoodle Jan 15, 2025
10820fc
Bump version
gazoodle Jan 15, 2025
7338866
Do development in a container now
gazoodle Jan 16, 2025
f5a0057
Major rewrork of packgen.py to get ready for github action to check l…
gazoodle Jan 17, 2025
826f65b
Bug fix in simulator to allow new HA snapshots to be loaded
gazoodle Jan 17, 2025
7e5219a
Some refactoring to try to remove sleep() in various places and be mo…
gazoodle Jan 19, 2025
2ed0e41
Remove obscure return value from wait_for_response and use property w…
gazoodle Jan 19, 2025
547ef61
A bit of tidy so the class is less clutter with RUFF issues.
gazoodle Jan 19, 2025
97785fd
Added full exception handling in task coroutines
gazoodle Jan 20, 2025
e7a1af7
Replace using internal _spa_state property with a function so that we…
gazoodle Jan 20, 2025
34e4e6e
Just a couple of asyncio.sleep() left that need handling
gazoodle Jan 20, 2025
019a3c1
More RUFF tidy
gazoodle Jan 21, 2025
d32c265
Remove remaining asycio.sleep() calls and replaced with the config_sl…
gazoodle Jan 21, 2025
4a7af69
Protect queue head from being None
gazoodle Jan 21, 2025
4509280
Tidy
gazoodle Jan 21, 2025
9b9735b
Remove obsolete constant ASYNCIO_SLEEP_TIMEOUT_FOR_YIELD
gazoodle Jan 21, 2025
0c2c89f
Command base -> async
gazoodle Jan 21, 2025
ad59ab5
Shell tidy
gazoodle Jan 21, 2025
1c6d2c6
Logging cleanup in shell clients
gazoodle Jan 22, 2025
aa8390e
Huge refactor to track down a deadlock
gazoodle Jan 23, 2025
90a64e5
Fix possible socket leak by making the pattern clean
gazoodle Jan 23, 2025
59e35ba
Update README
gazoodle Jan 23, 2025
1bc59fa
GeckoShell is now async in implementation and clienting
gazoodle Jan 23, 2025
d3e0eea
Working to get simulator moved to async API
gazoodle Jan 26, 2025
6f1f3fb
Refactor again moving to sync API removal
gazoodle Jan 27, 2025
0626643
All clients now using async API, time to delete the sync one
gazoodle Jan 27, 2025
f2f2e4b
Update README
gazoodle Jan 27, 2025
2e88eef
Fix issues created by checkin
gazoodle Jan 27, 2025
faecfb4
More check-in fixes
gazoodle Jan 27, 2025
0890cbb
Pickup
gazoodle Jan 27, 2025
3656c06
Merge pull request #56 from gazoodle/dev
gazoodle Jan 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading