pax_global_header 0000666 0000000 0000000 00000000064 14514431126 0014513 g ustar 00root root 0000000 0000000 52 comment=f7538e37f24bb7cb1ed2777adbee7b74725748b9
frenck-python-toonapi-f7538e3/ 0000775 0000000 0000000 00000000000 14514431126 0016317 5 ustar 00root root 0000000 0000000 frenck-python-toonapi-f7538e3/.editorconfig 0000664 0000000 0000000 00000000467 14514431126 0021003 0 ustar 00root root 0000000 0000000 root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
ident_size = 4
[*.md]
ident_size = 2
trim_trailing_whitespace = false
[*.json]
ident_size = 2
[{.gitignore,.gitkeep,.editorconfig}]
ident_size = 2
[Makefile]
ident_style = tab
frenck-python-toonapi-f7538e3/.flake8 0000664 0000000 0000000 00000000055 14514431126 0017472 0 ustar 00root root 0000000 0000000 [flake8]
max-line-length=88
ignore=D202,W503
frenck-python-toonapi-f7538e3/.gitattributes 0000664 0000000 0000000 00000000051 14514431126 0021206 0 ustar 00root root 0000000 0000000 * text eol=lf
*.py whitespace=error
frenck-python-toonapi-f7538e3/.github/ 0000775 0000000 0000000 00000000000 14514431126 0017657 5 ustar 00root root 0000000 0000000 frenck-python-toonapi-f7538e3/.github/FUNDING.yml 0000664 0000000 0000000 00000000106 14514431126 0021471 0 ustar 00root root 0000000 0000000 ---
github: frenck
patreon: frenck
custom: https://frenck.dev/donate/
frenck-python-toonapi-f7538e3/.github/workflows/ 0000775 0000000 0000000 00000000000 14514431126 0021714 5 ustar 00root root 0000000 0000000 frenck-python-toonapi-f7538e3/.github/workflows/ci.yml 0000664 0000000 0000000 00000001373 14514431126 0023036 0 ustar 00root root 0000000 0000000 ---
name: Continuous Integration
# yamllint disable-line rule:truthy
on: [push, pull_request]
jobs:
linting:
name: Linting
runs-on: ubuntu-latest
steps:
- name: Checking out code from GitHub
uses: actions/checkout@v4
- name: Set up Python 3.8
uses: actions/setup-python@v4
with:
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -r requirements_test.txt
pip install -r requirements.txt
pip install pre-commit
pip list
pre-commit --version
- name: Run pre-commit on all files
run: |
pre-commit run --all-files --show-diff-on-failure
frenck-python-toonapi-f7538e3/.gitignore 0000664 0000000 0000000 00000002577 14514431126 0020322 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# OSX useful to ignore
*.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# pyenv
.python-version
# virtualenv
.venv
venv/
ENV/
# mypy
.mypy_cache/
# Visual Studio Code
.vscode
# IntelliJ Idea family of suites
.idea
*.iml
## File-based project format:
*.ipr
*.iws
## mpeltonen/sbt-idea plugin
.idea_modules/
# PyBuilder
target/
# Cookiecutter
output/
python_boilerplate/
frenck-python-toonapi-f7538e3/.isort.cfg 0000664 0000000 0000000 00000000112 14514431126 0020210 0 ustar 00root root 0000000 0000000 [settings]
multi_line_output=3
line_length=88
include_trailing_comma=True
frenck-python-toonapi-f7538e3/.pre-commit-config.yaml 0000664 0000000 0000000 00000003322 14514431126 0022600 0 ustar 00root root 0000000 0000000 ---
repos:
- repo: https://github.com/psf/black
rev: 23.10.0
hooks:
- id: black
args: [--safe, --quiet, --target-version, py36]
- repo: https://github.com/asottile/blacken-docs
rev: 1.16.0
hooks:
- id: blacken-docs
additional_dependencies: [black==23.10.0]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: debug-statements
- id: check-docstring-first
- id: check-json
- id: check-yaml
- id: requirements-txt-fixer
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: fix-encoding-pragma
args: ["--remove"]
- id: check-ast
- id: detect-private-key
- id: forbid-new-submodules
- repo: https://github.com/pre-commit/pre-commit
rev: v3.5.0
hooks:
- id: validate_manifest
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.5.3
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/pylint-dev/pylint
rev: v3.0.1
hooks:
- id: pylint
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.6.1
hooks:
- id: mypy
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
- id: flake8
additional_dependencies: ["flake8-docstrings"]
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.32.0
hooks:
- id: yamllint
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: [--py38-plus]
frenck-python-toonapi-f7538e3/.yamllint 0000664 0000000 0000000 00000002356 14514431126 0020157 0 ustar 00root root 0000000 0000000 ---
rules:
braces:
level: error
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
brackets:
level: error
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
colons:
level: error
max-spaces-before: 0
max-spaces-after: 1
commas:
level: error
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
comments:
level: error
require-starting-space: true
min-spaces-from-content: 2
comments-indentation:
level: error
document-end:
level: error
present: false
document-start:
level: error
present: true
empty-lines:
level: error
max: 1
max-start: 0
max-end: 1
hyphens:
level: error
max-spaces-after: 1
indentation:
level: error
spaces: 2
indent-sequences: true
check-multi-line-strings: false
key-duplicates:
level: error
line-length:
level: warning
max: 120
allow-non-breakable-words: true
allow-non-breakable-inline-mappings: true
new-line-at-end-of-file:
level: error
new-lines:
level: error
type: unix
trailing-spaces:
level: error
truthy:
level: error
frenck-python-toonapi-f7538e3/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000006214 14514431126 0021121 0 ustar 00root root 0000000 0000000 # Code of conduct
## Our pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention
or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or
electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate
in a professional setting
## Our responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project lead at frenck@addons.community. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project lead is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
frenck-python-toonapi-f7538e3/CONTRIBUTING.md 0000664 0000000 0000000 00000002213 14514431126 0020546 0 ustar 00root root 0000000 0000000 # Contributing
When contributing to this repository, please first discuss the change you wish
to make via issue, email, or any other method with the owners of this repository
before making a change.
Please note we have a code of conduct, please follow it in all your interactions
with the project.
## Issues and feature requests
You've found a bug in the source code, a mistake in the documentation or maybe
you'd like a new feature? You can help us by submitting an issue to our
[GitHub Repository][github]. Before you create an issue, make sure you search
the archive, maybe your question was already answered.
Even better: You could submit a pull request with a fix / new feature!
## Pull request process
1. Search our repository for open or closed [pull requests][prs] that relates
to your submission. You don't want to duplicate effort.
1. You may merge the pull request in once you have the sign-off of two other
developers, or if you do not have permission to do that, you may request
the second reviewer to merge it for you.
[github]: https://github.com/frenck/python-toonapi/issues
[prs]: https://github.com/frenck/python-toonapi/pulls
frenck-python-toonapi-f7538e3/LICENSE.md 0000664 0000000 0000000 00000002060 14514431126 0017721 0 ustar 00root root 0000000 0000000 # MIT License
Copyright (c) 2020 Franck Nijhof
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
frenck-python-toonapi-f7538e3/MANIFEST.in 0000664 0000000 0000000 00000000120 14514431126 0020046 0 ustar 00root root 0000000 0000000 include README.md
include LICENSE.md
graft toonapi
recursive-exclude * *.py[co]
frenck-python-toonapi-f7538e3/Makefile 0000664 0000000 0000000 00000005650 14514431126 0017765 0 ustar 00root root 0000000 0000000 .DEFAULT_GOAL := help
export VENV := $(abspath venv)
export PATH := ${VENV}/bin:${PATH}
define BROWSER_PYSCRIPT
import os, webbrowser, sys
try:
from urllib import pathname2url
except:
from urllib.request import pathname2url
webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
endef
export BROWSER_PYSCRIPT
BROWSER := python -c "$$BROWSER_PYSCRIPT"
.PHONY: help
help: ## Shows this message.
@echo "Asynchronous Python client for the Quby ToonAPI."; \
echo; \
echo "Usage:"; \
awk -F ':|##' '/^[^\t].+?:.*?##/ {\
printf "\033[36m make %-30s\033[0m %s\n", $$1, $$NF \
}' $(MAKEFILE_LIST)
.PHONY: dev
dev: install-dev install ## Set up a development environment.
.PHONY: black
black: lint-black
.PHONY: lint
lint: lint-black lint-flake8 lint-pylint lint-mypy ## Run all linters.
.PHONY: lint-black
lint-black: ## Run linting using black & blacken-docs.
black --safe --target-version py36 toonapi tests examples; \
blacken-docs --target-version py36
.PHONY: lint-flake8
lint-flake8: ## Run linting using flake8 (pycodestyle/pydocstyle).
flake8 toonapi
.PHONY: lint-pylint
lint-pylint: ## Run linting using PyLint.
pylint toonapi
.PHONY: lint-mypy
lint-mypy: ## Run linting using MyPy.
mypy -p toonapi
.PHONY: test
test: ## Run tests quickly with the default Python.
pytest --cov-report html --cov-report term --cov-report xml:cov.xml --cov=toonapi .;
.PHONY: coverage
coverage: test ## Check code coverage quickly with the default Python.
$(BROWSER) htmlcov/index.html
.PHONY: install
install: clean ## Install the package to the active Python's site-packages.
pip install -Ur requirements.txt; \
pip install -e .;
.PHONY: clean clean-all
clean: clean-build clean-pyc clean-test ## Removes build, test, coverage and Python artifacts.
clean-all: clean-build clean-pyc clean-test clean-venv ## Removes all venv, build, test, coverage and Python artifacts.
.PHONY: clean-build
clean-build: ## Removes build artifacts.
rm -fr build/; \
rm -fr dist/; \
rm -fr .eggs/; \
find . -name '*.egg-info' -exec rm -fr {} +; \
find . -name '*.egg' -exec rm -fr {} +;
.PHONY: clean-pyc
clean-pyc: ## Removes Python file artifacts.
find . -name '*.pyc' -delete; \
find . -name '*.pyo' -delete; \
find . -name '*~' -delete; \
find . -name '__pycache__' -exec rm -fr {} +;
.PHONY: clean-test
clean-test: ## Removes test and coverage artifacts.
rm -f .coverage; \
rm -fr htmlcov/; \
rm -fr .pytest_cache;
.PHONY: clean-venv
clean-venv: ## Removes Python virtual environment artifacts.
rm -fr venv/;
.PHONY: dist
dist: clean ## Builds source and wheel package.
python setup.py sdist; \
python setup.py bdist_wheel; \
ls -l dist;
.PHONY: release
release: ## Release build on PyP
twine upload dist/*
.PHONY: venv
venv: clean-venv ## Create Python venv environment.
python3 -m venv venv;
.PHONY: install-dev
install-dev: clean
pip install -Ur requirements_dev.txt; \
pip install -Ur requirements_test.txt; \
pre-commit install;
frenck-python-toonapi-f7538e3/README.md 0000664 0000000 0000000 00000015215 14514431126 0017602 0 ustar 00root root 0000000 0000000 # Python: Asynchronous Python client for the Quby ToonAPI
[![GitHub Release][releases-shield]][releases]
![Project Stage][project-stage-shield]
![Project Maintenance][maintenance-shield]
[![License][license-shield]](LICENSE.md)
[![Build Status][build-shield]][build]
[![Code Coverage][codecov-shield]][codecov]
[![Code Quality][code-quality-shield]][code-quality]
[![Sponsor Frenck via GitHub Sponsors][github-sponsors-shield]][github-sponsors]
[![Support Frenck on Patreon][patreon-shield]][patreon]
Asynchronous Python client for the Quby ToonAPI.
## About
An asynchronous python client for the Qubby ToonAPI to control Quby thermostats.
These thermostats are also sold as:
- Eneco Toon
- Engie Electrabel Boxx
- Viesgo
They all use the same API endpoint, thus this library can be used.
This library is created to support the integration in
[Home Assistant](https://www.home-assistant.io).
## Installation
```bash
pip install toonapi
```
## Usage
```python
import asyncio
from toonapi import Toon
async def main():
"""Show example on using the ToonAPI."""
async with Toon(token="put-in-token-here") as toon:
agreements = await toon.agreements()
print(agreements)
await toon.activate_agreement(agreement=agreements[0])
status = await toon.update()
print(status.gas_usage)
print(status.thermostat)
print(status.power_usage)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```
## Changelog & Releases
This repository keeps a change log using [GitHub's releases][releases]
functionality. The format of the log is based on
[Keep a Changelog][keepchangelog].
Releases are based on [Semantic Versioning][semver], and use the format
of ``MAJOR.MINOR.PATCH``. In a nutshell, the version will be incremented
based on the following:
- ``MAJOR``: Incompatible or major changes.
- ``MINOR``: Backwards-compatible new features and enhancements.
- ``PATCH``: Backwards-compatible bugfixes and package updates.
## Contributing
This is an active open-source project. We are always open to people who want to
use the code or contribute to it.
We've set up a separate document for our
[contribution guidelines](CONTRIBUTING.md).
Thank you for being involved! :heart_eyes:
## Setting up development environment
In case you'd like to contribute, a `Makefile` has been included to ensure a
quick start.
```bash
make venv
source ./venv/bin/activate
make dev
```
Now you can start developing, run `make` without arguments to get an overview
of all make goals that are available (including description):
```bash
$ make
Asynchronous Python client for the Quby ToonAPI.
Usage:
make help Shows this message.
make dev Set up a development environment.
make lint Run all linters.
make lint-black Run linting using black & blacken-docs.
make lint-flake8 Run linting using flake8 (pycodestyle/pydocstyle).
make lint-pylint Run linting using PyLint.
make lint-mypy Run linting using MyPy.
make test Run tests quickly with the default Python.
make coverage Check code coverage quickly with the default Python.
make install Install the package to the active Python's site-packages.
make clean Removes build, test, coverage and Python artifacts.
make clean-all Removes all venv, build, test, coverage and Python artifacts.
make clean-build Removes build artifacts.
make clean-pyc Removes Python file artifacts.
make clean-test Removes test and coverage artifacts.
make clean-venv Removes Python virtual environment artifacts.
make dist Builds source and wheel package.
make release Release build on PyP
make venv Create Python venv environment.
```
## Authors & contributors
The original setup of this repository is by [Franck Nijhof][frenck].
For a full list of all authors and contributors,
check [the contributor's page][contributors].
## License
MIT License
Copyright (c) 2020 Franck Nijhof
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
[build-shield]: https://github.com/frenck/python-toonapi/workflows/Continuous%20Integration/badge.svg
[build]: https://github.com/frenck/python-toonapi/actions
[code-quality-shield]: https://img.shields.io/lgtm/grade/python/g/frenck/python-toonapi.svg?logo=lgtm&logoWidth=18
[code-quality]: https://lgtm.com/projects/g/frenck/python-toonapi/context:python
[codecov-shield]: https://codecov.io/gh/frenck/python-toonapi/branch/master/graph/badge.svg
[codecov]: https://codecov.io/gh/frenck/python-toonapi
[contributors]: https://github.com/frenck/python-toonapi/graphs/contributors
[frenck]: https://github.com/frenck
[github-sponsors-shield]: https://frenck.dev/wp-content/uploads/2019/12/github_sponsor.png
[github-sponsors]: https://github.com/sponsors/frenck
[keepchangelog]: http://keepachangelog.com/en/1.0.0/
[license-shield]: https://img.shields.io/github/license/frenck/python-toonapi.svg
[maintenance-shield]: https://img.shields.io/maintenance/yes/2020.svg
[patreon-shield]: https://frenck.dev/wp-content/uploads/2019/12/patreon.png
[patreon]: https://www.patreon.com/frenck
[project-stage-shield]: https://img.shields.io/badge/project%20stage-experimental-yellow.svg
[releases-shield]: https://img.shields.io/github/release/frenck/python-toonapi.svg
[releases]: https://github.com/frenck/python-toonapi/releases
[semver]: http://semver.org/spec/v2.0.0.html
frenck-python-toonapi-f7538e3/cov.xml 0000664 0000000 0000000 00000026473 14514431126 0017644 0 ustar 00root root 0000000 0000000
/home/frenck/code/frenck/python-toonapi/toonapi
frenck-python-toonapi-f7538e3/examples/ 0000775 0000000 0000000 00000000000 14514431126 0020135 5 ustar 00root root 0000000 0000000 frenck-python-toonapi-f7538e3/examples/control.py 0000664 0000000 0000000 00000001143 14514431126 0022166 0 ustar 00root root 0000000 0000000 # pylint: disable=W0621
"""Asynchronous Python client for Quby ToonAPI."""
import asyncio
from toonapi import Toon
async def main():
"""Show example on using the ToonAPI."""
async with Toon(token="put-in-token-here") as toon:
agreements = await toon.agreements()
print(agreements)
await toon.activate_agreement(agreement=agreements[0])
status = await toon.update()
print(status.gas_usage)
print(status.thermostat)
print(status.power_usage)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
frenck-python-toonapi-f7538e3/mypi.ini 0000664 0000000 0000000 00000001553 14514431126 0020002 0 ustar 00root root 0000000 0000000 [mypy]
# Specify the target platform details in config, so your developers are
# free to run mypy on Windows, Linux, or macOS and get consistent
# results.
python_version=3.7
platform=linux
# flake8-mypy expects the two following for sensible formatting
show_column_numbers=True
# show error messages from unrelated files
follow_imports=normal
# suppress errors about unsatisfied imports
ignore_missing_imports=True
# be strict
check_untyped_defs=True
disallow_untyped_calls=True
disallow_untyped_defs=True
no_implicit_optional=True
strict_equality=True
strict_optional=True
warn_incomplete_stub=True
warn_no_return=True
warn_redundant_casts=True
warn_redundant_casts=True
warn_return_any=True
warn_return_any=True
warn_unused_configs=True
warn_unused_ignores=True
warn_unused_ignores=True
# No incremental mode
cache_dir=/dev/null
[mypy-aiohttp.*]
follow_imports=skip
frenck-python-toonapi-f7538e3/pylintrc 0000664 0000000 0000000 00000023320 14514431126 0020106 0 ustar 00root root 0000000 0000000 [MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=tests
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=1
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=
attribute-defined-outside-init,
duplicate-code,
fixme,
import-error,
invalid-name,
missing-docstring,
protected-access,
too-few-public-methods,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-public-methods,
too-many-return-statements,
too-many-statements,
unnecessary-pass,
# handled by black
format
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=yes
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=88
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )??$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# Maximum number of lines in a module
max-module-lines=2000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,}$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=__.*__
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# List of decorators that define properties, such as abc.abstractproperty.
property-classes=abc.abstractproperty
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis
ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# List of decorators that create context managers from functions, such as
# contextlib.contextmanager.
contextmanager-decorators=contextlib.contextmanager
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[DESIGN]
# Maximum number of arguments for function / method
max-args=10
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=25
# Maximum number of return / yield for function / method body
max-returns=11
# Maximum number of branch for function / method body
max-branches=26
# Maximum number of statements in function / method body
max-statements=100
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=11
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=25
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
frenck-python-toonapi-f7538e3/requirements.txt 0000664 0000000 0000000 00000000053 14514431126 0021601 0 ustar 00root root 0000000 0000000 aiohttp==3.8.5
backoff==1.10.0
yarl==1.4.2
frenck-python-toonapi-f7538e3/requirements_dev.txt 0000664 0000000 0000000 00000000174 14514431126 0022443 0 ustar 00root root 0000000 0000000 autopep8==1.5.3
black==23.10.0
blacken-docs==1.16.0
pip==23.3
pre-commit==3.5.0
twine==4.0.2
wheel==0.41.2
yamllint==1.32.0
frenck-python-toonapi-f7538e3/requirements_test.txt 0000664 0000000 0000000 00000000250 14514431126 0022637 0 ustar 00root root 0000000 0000000 aresponses==2.1.6
coverage==7.3.2
flake8==6.1.0
flake8-docstrings==1.7.0
isort==5.12.0
mypy==1.6.1
pylint==3.0.1
pytest==7.4.2
pytest-asyncio==0.21.1
pytest-cov==4.1.0
frenck-python-toonapi-f7538e3/setup.py 0000664 0000000 0000000 00000003735 14514431126 0020041 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""The setup script."""
import os
import re
import sys
from setuptools import find_packages, setup
def get_version():
"""Get current version from code."""
regex = r"__version__\s=\s\"(?P[\d\.]+?)\""
path = ("toonapi", "__version__.py")
return re.search(regex, read(*path)).group("version")
def read(*parts):
"""Read file."""
filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), *parts)
sys.stdout.write(filename)
with open(filename, encoding="utf-8") as fp:
return fp.read()
with open("README.md", encoding="utf-8") as readme_file:
readme = readme_file.read()
setup(
author="Franck Nijhof",
author_email="opensource@frenck.dev",
classifiers=[
"Development Status :: 4 - Beta",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3",
"Topic :: Software Development :: Libraries :: Python Modules",
],
description="Asynchronous Python client for the Quby ToonAPI.",
include_package_data=True,
install_requires=["aiohttp>=3.0.0", "backoff>=1.9.0", "yarl"],
keywords=[
"toon",
"quby",
"eneco",
"boxx",
"engie",
"electrabel",
"viesgo",
"toonapi",
"api",
"async",
"client",
],
license="MIT license",
long_description_content_type="text/markdown",
long_description=readme,
name="toonapi",
packages=find_packages(include=["toonapi"]),
test_suite="tests",
url="https://github.com/frenck/python-toonapi",
version=get_version(),
zip_safe=False,
)
frenck-python-toonapi-f7538e3/tests/ 0000775 0000000 0000000 00000000000 14514431126 0017461 5 ustar 00root root 0000000 0000000 frenck-python-toonapi-f7538e3/tests/__init__.py 0000664 0000000 0000000 00000000067 14514431126 0021575 0 ustar 00root root 0000000 0000000 """Asynchronous Python client for the Quby ToonAPI."""
frenck-python-toonapi-f7538e3/toonapi/ 0000775 0000000 0000000 00000000000 14514431126 0017770 5 ustar 00root root 0000000 0000000 frenck-python-toonapi-f7538e3/toonapi/__init__.py 0000664 0000000 0000000 00000001014 14514431126 0022075 0 ustar 00root root 0000000 0000000 """Asynchronous Python client for Quby."""
from .const import ( # noqa
ACTIVE_STATE_AWAY,
ACTIVE_STATE_COMFORT,
ACTIVE_STATE_HOLIDAY,
ACTIVE_STATE_HOME,
ACTIVE_STATE_NONE,
ACTIVE_STATE_OFF,
ACTIVE_STATE_SLEEP,
BURNER_STATE_OFF,
BURNER_STATE_ON,
BURNER_STATE_PREHEATING,
BURNER_STATE_TAP_WATER,
PROGRAM_STATE_OFF,
PROGRAM_STATE_ON,
PROGRAM_STATE_OVERRIDE,
)
from .models import Agreement, Status # noqa
from .toon import Toon, ToonConnectionError, ToonError # noqa
frenck-python-toonapi-f7538e3/toonapi/__version__.py 0000664 0000000 0000000 00000000112 14514431126 0022615 0 ustar 00root root 0000000 0000000 """Asynchronous Python client the Quby ToonAPI."""
__version__ = "0.3.0"
frenck-python-toonapi-f7538e3/toonapi/const.py 0000664 0000000 0000000 00000001005 14514431126 0021464 0 ustar 00root root 0000000 0000000 """Constants for the Quby ToonAPI."""
ACTIVE_STATE_AWAY = 3
ACTIVE_STATE_COMFORT = 0
ACTIVE_STATE_HOLIDAY = 4
ACTIVE_STATE_HOME = 1
ACTIVE_STATE_NONE = -1
ACTIVE_STATE_OFF = -1
ACTIVE_STATE_SLEEP = 2
BURNER_STATE_OFF = 0
BURNER_STATE_ON = 1
BURNER_STATE_PREHEATING = 3
BURNER_STATE_TAP_WATER = 2
HEATING_TYPE_GAS = 1
PROGRAM_STATE_OFF = 0
PROGRAM_STATE_ON = 1
PROGRAM_STATE_OVERRIDE = 2
TOON_API_BASE_PATH = "/"
TOON_API_HOST = "api.toon.eu"
TOON_API_PORT = 443
TOON_API_SCHEME = "https"
TOON_API_VERSION = "v3"
frenck-python-toonapi-f7538e3/toonapi/exceptions.py 0000664 0000000 0000000 00000000574 14514431126 0022531 0 ustar 00root root 0000000 0000000 """Exceptions for the Quby ToonAPI."""
class ToonError(Exception):
"""Generic ToonAPI exception."""
class ToonConnectionError(ToonError):
"""ToonAPI connection exception."""
class ToonConnectionTimeoutError(ToonConnectionError):
"""ToonAPI connection timeout exception."""
class ToonRateLimitError(ToonConnectionError):
"""ToonAPI Rate Limit exception."""
frenck-python-toonapi-f7538e3/toonapi/models.py 0000664 0000000 0000000 00000037720 14514431126 0021636 0 ustar 00root root 0000000 0000000 """Models for the Quby ToonAPI."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Callable
from .const import (
ACTIVE_STATE_HOLIDAY,
BURNER_STATE_ON,
BURNER_STATE_PREHEATING,
BURNER_STATE_TAP_WATER,
PROGRAM_STATE_ON,
PROGRAM_STATE_OVERRIDE,
)
from .util import (
convert_boolean,
convert_cm3,
convert_datetime,
convert_kwh,
convert_lmin,
convert_m3,
convert_negative_none,
convert_temperature,
)
def process_data(
data: dict[str, Any],
key: str,
current_value: Any,
conversion: Callable[[Any], Any] | None = None,
) -> Any:
"""test."""
if key not in data:
return current_value
if data[key] is None:
return current_value
if conversion is None:
return data[key]
return conversion(data[key])
@dataclass
class Agreement:
"""Object holding a Toon customer utility Agreement."""
agreement_id_checksum: str | None = None
agreement_id: str | None = None
city: str | None = None
display_common_name: str | None = None
display_hardware_version: str | None = None
display_software_version: str | None = None
heating_type: str | None = None
house_number: str | None = None
is_toon_solar: bool | None = None
is_toonly: bool | None = None
postal_code: str | None = None
street: str | None = None
@staticmethod
def from_dict(data: dict[str, Any]) -> Agreement:
"""Return an Agreement object from a data dictionary."""
return Agreement(
agreement_id_checksum=data.get("agreementIdChecksum"),
agreement_id=data.get("agreementId"),
city=data.get("city"),
display_common_name=data.get("displayCommonName"),
display_hardware_version=data.get("displayHardwareVersion"),
display_software_version=data.get("displaySoftwareVersion"),
heating_type=data.get("heatingType"),
house_number=data.get("houseNumber"),
is_toon_solar=data.get("isToonSolar"),
is_toonly=data.get("isToonly"),
postal_code=data.get("postalCode"),
street=data.get("street"),
)
@dataclass
class ThermostatInfo:
"""Object holding Toon thermostat information."""
active_state: int | None = None
boiler_module_connected: bool | None = None
burner_state: int | None = None
current_display_temperature: float | None = None
current_humidity: int | None = None
current_modulation_level: int | None = None
current_setpoint: float | None = None
error_found: bool | None = None
has_boiler_fault: bool | None = None
have_opentherm_boiler: bool | None = None
holiday_mode: bool | None = None
next_program: int | None = None
next_setpoint: float | None = None
next_state: int | None = None
next_time: datetime | None = None
opentherm_communication_error: bool | None = None
program_state: int | None = None
real_setpoint: float | None = None
set_by_load_shifthing: int | None = None
last_updated_from_display: datetime | None = None
last_updated: datetime = datetime.now(tz=timezone.utc).replace(tzinfo=None)
@property
def burner(self) -> bool | None:
"""Return if burner is on based on its state."""
if self.burner_state is None:
return None
return bool(self.burner_state)
@property
def hot_tapwater(self) -> bool | None:
"""Return if burner is on based on its state."""
if self.burner_state is None:
return None
return self.burner_state == BURNER_STATE_TAP_WATER
@property
def heating(self) -> bool | None:
"""Return if burner is pre heating based on its state."""
if self.burner_state is None:
return None
return self.burner_state == BURNER_STATE_ON
@property
def pre_heating(self) -> bool | None:
"""Return if burner is pre heating based on its state."""
if self.burner_state is None:
return None
return self.burner_state == BURNER_STATE_PREHEATING
@property
def program(self) -> bool | None:
"""Return if program mode is turned on."""
if self.program_state is None:
return None
return self.program_state in [PROGRAM_STATE_ON, PROGRAM_STATE_OVERRIDE]
@property
def program_overridden(self) -> bool | None:
"""Return if program mode is overriden."""
if self.program_state is None:
return None
return self.program_state == PROGRAM_STATE_OVERRIDE
def update_from_dict(self, data: dict[str, Any]) -> None:
"""Update this ThermostatInfo object with data from a dictionary."""
self.active_state = process_data(
data, "activeState", self.active_state, convert_negative_none
)
self.boiler_module_connected = process_data(
data, "boilerModuleConnected", self.boiler_module_connected, convert_boolean
)
self.burner_state = process_data(data, "burnerInfo", self.burner_state, int)
self.current_display_temperature = process_data(
data,
"currentDisplayTemp",
self.current_display_temperature,
convert_temperature,
)
self.current_humidity = process_data(
data, "currentHumidity", self.current_humidity
)
self.current_modulation_level = process_data(
data, "currentModulationLevel", self.current_modulation_level
)
self.current_setpoint = process_data(
data, "currentSetpoint", self.current_setpoint, convert_temperature
)
self.error_found = process_data(
data, "errorFound", self.error_found, lambda x: x != 255
)
self.has_boiler_fault = process_data(
data, "hasBoilerFault", self.has_boiler_fault, convert_boolean
)
self.have_opentherm_boiler = process_data(
data, "haveOTBoiler", self.have_opentherm_boiler, convert_boolean
)
self.holiday_mode = process_data(
data, "activeState", self.holiday_mode, lambda x: x == ACTIVE_STATE_HOLIDAY
)
self.next_program = process_data(
data, "nextProgram", self.next_program, convert_negative_none
)
self.next_setpoint = process_data(
data, "nextSetpoint", self.next_setpoint, convert_temperature
)
self.next_state = process_data(
data, "nextState", self.next_state, convert_negative_none
)
self.next_time = process_data(
data, "nextTime", self.next_state, convert_datetime
)
self.opentherm_communication_error = process_data(
data, "otCommError", self.opentherm_communication_error, convert_boolean
)
self.program_state = process_data(data, "programState", self.program_state)
self.real_setpoint = process_data(
data, "realSetpoint", self.real_setpoint, convert_temperature
)
self.set_by_load_shifthing = process_data(
data, "setByLoadShifting", self.set_by_load_shifthing, convert_boolean
)
self.last_updated_from_display = process_data(
data,
"lastUpdatedFromDisplay",
self.last_updated_from_display,
convert_datetime,
)
self.last_updated = datetime.now(tz=timezone.utc).replace(tzinfo=None)
@dataclass
class PowerUsage:
"""Object holding Toon power usage information."""
average_produced: float | None = None
average_solar: float | None = None
average: float | None = None
current_produced: int | None = None
current_solar: int | None = None
current: int | None = None
day_average: float | None = None
day_cost: float | None = None
day_high_usage: float | None = None
day_low_usage: float | None = None
day_max_solar: int | None = None
day_produced_solar: float | None = None
is_smart: bool | None = None
meter_high: float | None = None
meter_low: float | None = None
meter_produced_high: float | None = None
meter_produced_low: float | None = None
last_updated_from_display: datetime | None = None
last_updated: datetime = datetime.now(tz=timezone.utc).replace(tzinfo=None)
@property
def day_usage(self) -> float | None:
"""Calculate day total usage."""
if self.day_high_usage is None or self.day_low_usage is None:
return None
return round(self.day_high_usage + self.day_low_usage, 2)
@property
def day_to_grid_usage(self) -> float | None:
"""Calculate day total to grid."""
if self.day_usage is None or self.day_produced_solar is None:
return None
return abs(min(0.0, round(self.day_usage - self.day_produced_solar, 2)))
@property
def day_from_grid_usage(self) -> float | None:
"""Calculate day total to grid."""
if self.day_produced_solar is None or self.day_usage is None:
return None
return abs(min(0.0, round(self.day_produced_solar - self.day_usage, 2)))
@property
def current_covered_by_solar(self) -> int | None:
"""Calculate current solar covering current usage."""
if self.current_solar is None or self.current is None:
return None
if self.current == 0:
return 0
return min(100, round((self.current_solar / self.current) * 100))
def update_from_dict(self, data: dict[str, Any]) -> None:
"""Update this PowerUsage object with data from a dictionary."""
self.average = process_data(data, "avgValue", self.average)
self.average_produced = process_data(
data, "avgProduValue", self.average_produced
)
self.average_solar = process_data(data, "avgSolarValue", self.average_solar)
self.current = process_data(data, "value", self.current, round)
self.current_produced = process_data(
data, "valueProduced", self.current_produced, round
)
self.current_solar = process_data(data, "valueSolar", self.current_solar, round)
self.day_average = process_data(
data, "avgDayValue", self.day_average, convert_kwh
)
self.day_cost = process_data(data, "dayCost", self.day_cost)
self.day_high_usage = process_data(
data, "dayUsage", self.day_high_usage, convert_kwh
)
self.day_low_usage = process_data(
data, "dayLowUsage", self.day_low_usage, convert_kwh
)
self.day_max_solar = process_data(data, "maxSolar", self.day_max_solar)
self.day_produced_solar = process_data(
data, "solarProducedToday", self.day_produced_solar, convert_kwh
)
self.is_smart = process_data(data, "isSmart", self.is_smart, convert_boolean)
self.meter_high = process_data(
data, "meterReading", self.meter_high, convert_kwh
)
self.meter_low = process_data(
data, "meterReadingLow", self.meter_low, convert_kwh
)
self.meter_produced_high = process_data(
data, "meterReadingProdu", self.meter_high, convert_kwh
)
self.meter_produced_low = process_data(
data, "meterReadingLowProdu", self.meter_produced_low, convert_kwh
)
self.last_updated_from_display = process_data(
data,
"lastUpdatedFromDisplay",
self.last_updated_from_display,
convert_datetime,
)
self.last_updated = datetime.now(tz=timezone.utc).replace(tzinfo=None)
@dataclass
class GasUsage:
"""Object holding Toon gas usage information."""
average: float | None = None
current: float | None = None
day_average: float | None = None
day_cost: float | None = None
day_usage: float | None = None
is_smart: bool | None = None
meter: float | None = None
last_updated_from_display: datetime | None = None
last_updated: datetime = datetime.now(tz=timezone.utc).replace(tzinfo=None)
def update_from_dict(self, data: dict[str, Any]) -> None:
"""Update this GasUsage object with data from a dictionary."""
self.average = process_data(data, "avgValue", self.average, convert_cm3)
self.current = process_data(data, "value", self.current, convert_cm3)
self.day_average = process_data(
data, "avgDayValue", self.day_average, convert_cm3
)
self.day_cost = process_data(data, "dayCost", self.day_cost)
self.day_usage = process_data(data, "dayUsage", self.day_usage, convert_cm3)
self.is_smart = process_data(data, "isSmart", self.is_smart, convert_boolean)
self.meter = process_data(data, "meterReading", self.meter, convert_cm3)
self.last_updated_from_display = process_data(
data,
"lastUpdatedFromDisplay",
self.last_updated_from_display,
convert_datetime,
)
self.last_update = datetime.now(tz=timezone.utc).replace(tzinfo=None)
@dataclass
class WaterUsage:
"""Object holding Toon water usage information."""
average: float | None = None
current: float | None = None
day_average: float | None = None
day_cost: float | None = None
day_usage: float | None = None
installed: bool | None = None
is_smart: bool | None = None
meter: float | None = None
last_updated_from_display: datetime | None = None
last_updated = datetime.now(tz=timezone.utc).replace(tzinfo=None)
def update_from_dict(self, data: dict[str, Any]) -> None:
"""Update this WaterUsage object with data from a dictionary."""
self.average = process_data(data, "avgValue", self.average, convert_lmin)
self.current = process_data(data, "value", self.current, convert_lmin)
self.day_average = process_data(
data, "avgDayValue", self.day_average, convert_m3
)
self.day_cost = process_data(data, "dayCost", self.day_cost)
self.day_usage = process_data(data, "dayUsage", self.day_usage, convert_m3)
self.installed = process_data(
data, "installed", self.installed, convert_boolean
)
self.is_smart = process_data(data, "isSmart", self.is_smart, convert_boolean)
self.meter = process_data(data, "meterReading", self.meter, convert_m3)
self.last_updated_from_display = process_data(
data,
"lastUpdatedFromDisplay",
self.last_updated_from_display,
convert_datetime,
)
self.last_update = datetime.now(tz=timezone.utc).replace(tzinfo=None)
class Status:
"""Object holding all status information for this ToonAPI instance."""
agreement: Agreement
thermostat: ThermostatInfo = ThermostatInfo()
power_usage: PowerUsage = PowerUsage()
gas_usage: GasUsage = GasUsage()
water_usage: WaterUsage = WaterUsage()
last_updated_from_display: datetime | None = None
last_updated: datetime = datetime.now(tz=timezone.utc).replace(tzinfo=None)
server_time: datetime | None = None
def __init__(self, agreement: Agreement):
"""Initialize an empty ToonAPI Status class."""
self.agreement = agreement
def update_from_dict(self, data: dict[str, Any]) -> Status:
"""Update the status object with data received from the ToonAPI."""
if "thermostatInfo" in data:
self.thermostat.update_from_dict(data["thermostatInfo"])
if "powerUsage" in data:
self.power_usage.update_from_dict(data["powerUsage"])
if "gasUsage" in data:
self.gas_usage.update_from_dict(data["gasUsage"])
if "waterUsage" in data:
self.water_usage.update_from_dict(data["waterUsage"])
if "lastUpdateFromDisplay" in data:
self.last_updated_from_display = convert_datetime(
data["lastUpdateFromDisplay"]
)
if "serverTime" in data:
self.server_time = convert_datetime(data["serverTime"])
self.last_updated = datetime.now(tz=timezone.utc).replace(tzinfo=None)
return self
frenck-python-toonapi-f7538e3/toonapi/toon.py 0000664 0000000 0000000 00000021177 14514431126 0021331 0 ustar 00root root 0000000 0000000 """Asynchronous Python client for Quby ToonAPI."""
from __future__ import annotations
import asyncio
import json
import socket
from typing import Any, Awaitable, Callable
import aiohttp
import async_timeout
import backoff
from yarl import URL
from .__version__ import __version__
from .const import (
ACTIVE_STATE_OFF,
PROGRAM_STATE_OVERRIDE,
TOON_API_BASE_PATH,
TOON_API_HOST,
TOON_API_PORT,
TOON_API_SCHEME,
TOON_API_VERSION,
)
from .exceptions import (
ToonConnectionError,
ToonConnectionTimeoutError,
ToonError,
ToonRateLimitError,
)
from .models import Agreement, Status
class Toon:
"""Main class for handling connections with the Quby ToonAPI."""
agreement_id: str | None = None
_agreements: list[Agreement] | None = None
_status: Status | None = None
_close_session: bool = False
_webhook_refresh_timer_task: asyncio.TimerHandle | None = None
_webhook_url: str | None = None
def __init__(
self,
*,
request_timeout: int = 8,
session: aiohttp.client.ClientSession | None = None,
token_refresh_method: Callable[[], Awaitable[str]] | None = None,
token: str,
user_agent: str | None = None,
) -> None:
"""Initialize connection with the Quby ToonAPI."""
self._session = session
self.request_timeout = request_timeout
self.user_agent = user_agent
self.token = token
self.token_refresh_method = token_refresh_method
if user_agent is None:
self.user_agent = f"PythonToonAPI/{__version__}"
@backoff.on_exception(backoff.expo, ToonConnectionError, max_tries=3, logger=None)
@backoff.on_exception(
backoff.expo, ToonRateLimitError, base=60, max_tries=6, logger=None
)
async def _request(
self,
uri: str = "",
*,
data: Any | None = None,
method: str = "GET",
no_agreement: bool = False,
) -> Any:
"""Handle a request to the Quby ToonAPI."""
if self.token_refresh_method is not None:
self.token = await self.token_refresh_method()
if self._status is None and self.agreement_id and not no_agreement:
await self.activate_agreement(
agreement_id=self.agreement_id,
)
url = URL.build(
scheme=TOON_API_SCHEME,
host=TOON_API_HOST,
port=TOON_API_PORT,
path=TOON_API_BASE_PATH,
).join(URL(uri))
headers = {
"Authorization": f"Bearer {self.token}",
"User-Agent": self.user_agent,
"Accept": "application/json",
}
if not no_agreement and self._status is not None:
headers.update(
{
"X-Common-Name": self._status.agreement.display_common_name,
"X-Agreement-ID": self._status.agreement.agreement_id,
}
)
if self._session is None:
self._session = aiohttp.ClientSession()
self._close_session = True
try:
with async_timeout.timeout(self.request_timeout):
response = await self._session.request(
method,
url,
json=data,
headers=headers,
ssl=True,
)
except asyncio.TimeoutError as exception:
raise ToonConnectionTimeoutError(
"Timeout occurred while connecting to the Quby ToonAPI"
) from exception
except (aiohttp.ClientError, socket.gaierror) as exception:
raise ToonConnectionError(
"Error occurred while communicating with the Quby ToonAPI"
) from exception
content_type = response.headers.get("Content-Type", "")
# Error handling
if (response.status // 100) in [4, 5]:
contents = await response.read()
response.close()
if response.status == 429:
raise ToonRateLimitError(
"Rate limit error has occurred with the Quby ToonAPI"
)
if content_type == "application/json":
raise ToonError(response.status, json.loads(contents.decode("utf8")))
raise ToonError(response.status, {"message": contents.decode("utf8")})
# Handle empty response
if response.status == 204:
return
if "application/json" in content_type:
return await response.json()
return await response.text()
async def activate_agreement(
self,
*,
agreement_id: str | None = None,
display_common_name: str | None = None,
agreement: Agreement | None = None,
) -> Agreement:
"""Set the active agreement for this Toon instance."""
if self._agreements is None:
await self.agreements()
if not self._agreements:
raise ToonError("No agreements found on linked account")
for known_agreement in self._agreements:
if (
known_agreement == agreement
or known_agreement.agreement_id == agreement_id
or known_agreement.display_common_name == display_common_name
):
self._status = Status(agreement=known_agreement)
self.agreement_id = known_agreement.agreement_id
self.display_common_name = known_agreement.display_common_name
return known_agreement
raise ToonError("Agreement could not be found on the linked account")
async def agreements(self, force_update: bool = False) -> list[Agreement]:
"""Return the agreement(s) that are associated with the utility customer."""
if self._agreements is None or force_update:
agreements = await self._request(
f"/toon/{TOON_API_VERSION}/agreements", no_agreement=True
)
self._agreements = [
Agreement.from_dict(agreement) for agreement in agreements
]
return self._agreements
async def update(self, data: dict[str, Any] | None = None) -> Status | None:
"""Get all information in a single call."""
assert self._status
if data is None:
data = await self._request(
f"/toon/{TOON_API_VERSION}/{self.agreement_id}/status"
)
return self._status.update_from_dict(data)
async def set_current_setpoint(self, temperature: float) -> None:
"""Set the target temperature for the thermostat."""
assert self._status
data = {
"currentSetpoint": round(temperature * 100),
"programState": PROGRAM_STATE_OVERRIDE,
"activeState": ACTIVE_STATE_OFF,
}
await self._request(
f"/toon/{TOON_API_VERSION}/{self.agreement_id}/thermostat",
method="PUT",
data=data,
)
self._status.thermostat.update_from_dict(data)
async def set_active_state(
self, active_state: int, program_state: int = PROGRAM_STATE_OVERRIDE
) -> None:
""".."""
assert self._status
data = {"programState": program_state, "activeState": active_state}
await self._request(
f"/toon/{TOON_API_VERSION}/{self.agreement_id}/thermostat",
method="PUT",
data=data,
)
self._status.thermostat.update_from_dict(data)
async def subscribe_webhook(self, application_id: str, url: str) -> None:
"""Register a webhook with Toon for live updates."""
# Unregister old webhooks from this application ID
await self.unsubscribe_webhook(application_id)
# Register webhook
await self._request(
f"/toon/{TOON_API_VERSION}/{self.agreement_id}/webhooks",
method="POST",
data={
"applicationId": application_id,
"callbackUrl": url,
"subscribedActions": ["BoilerErrorInfo", "PowerUsage", "Thermostat"],
},
)
async def unsubscribe_webhook(self, application_id: str) -> None:
"""Delete all webhooks for this application ID."""
await self._request(
f"/toon/{TOON_API_VERSION}/{self.agreement_id}/webhooks/{application_id}",
method="DELETE",
)
async def close(self) -> None:
"""Close open client session."""
if self._session and self._close_session:
await self._session.close()
async def __aenter__(self) -> Toon:
"""Async enter."""
return self
async def __aexit__(self, *exc_info) -> None:
"""Async exit."""
await self.close()
frenck-python-toonapi-f7538e3/toonapi/util.py 0000664 0000000 0000000 00000003257 14514431126 0021326 0 ustar 00root root 0000000 0000000 """Collection of small utility functions for ToonAPI."""
from datetime import datetime
from typing import Any, Optional
def convert_temperature(temperature: int) -> Optional[float]:
"""Convert a temperature value from the ToonAPI to a float value."""
if temperature is None:
return None
return temperature / 100.0
def convert_boolean(value: Any) -> Optional[bool]:
"""Convert a value from the ToonAPI to a boolean."""
if value is None:
return None
return bool(value)
def convert_datetime(timestamp: int) -> datetime:
"""Convert a java microseconds timestamp from the ToonAPI to a datetime."""
return datetime.utcfromtimestamp(timestamp // 1000.0).replace(
microsecond=timestamp % 1000 * 1000
)
def convert_kwh(value: int) -> Optional[float]:
"""Convert a Wh value from the ToonAPI to a kWH value."""
if value is None:
return None
return round(float(value) / 1000.0, 2)
def convert_cm3(value: int) -> Optional[float]:
"""Convert a value from the ToonAPI to a CM3 value."""
if value is None:
return None
return round(float(value) / 1000.0, 2)
def convert_negative_none(value: int) -> Optional[int]:
"""Convert an negative int value from the ToonAPI to a NoneType."""
return None if value < 0 else value
def convert_m3(value: int) -> Optional[float]:
"""Convert a value from the ToonAPI to a M3 value."""
if value is None:
return None
return round(float(value) / 1000.0, 2)
def convert_lmin(value: int) -> Optional[float]:
"""Convert a value from the ToonAPI to a L/MINUTE value."""
if value is None:
return None
return round(float(value) / 60.0, 1)