emerald-hws 0.0.10__tar.gz → 0.0.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- emerald_hws-0.0.12/.github/dependabot.yml +13 -0
- emerald_hws-0.0.12/.github/workflows/lint.yml +30 -0
- emerald_hws-0.0.12/.github/workflows/publish.yml +222 -0
- emerald_hws-0.0.12/.github/workflows/smoke-test.yml +34 -0
- emerald_hws-0.0.12/.gitignore +160 -0
- emerald_hws-0.0.12/PKG-INFO +80 -0
- emerald_hws-0.0.12/README.md +62 -0
- emerald_hws-0.0.12/pyproject.toml +65 -0
- emerald_hws-0.0.12/requirements.txt +8 -0
- emerald_hws-0.0.12/src/emerald_hws/__init__.py +3 -0
- {emerald_hws-0.0.10 → emerald_hws-0.0.12}/src/emerald_hws/emeraldhws.py +148 -17
- emerald_hws-0.0.12/src/emerald_hws.egg-info/PKG-INFO +80 -0
- {emerald_hws-0.0.10 → emerald_hws-0.0.12}/src/emerald_hws.egg-info/SOURCES.txt +6 -0
- emerald_hws-0.0.12/src/emerald_hws.egg-info/requires.txt +6 -0
- emerald_hws-0.0.10/PKG-INFO +0 -18
- emerald_hws-0.0.10/README.md +0 -2
- emerald_hws-0.0.10/pyproject.toml +0 -30
- emerald_hws-0.0.10/src/emerald_hws/__init__.py +0 -1
- emerald_hws-0.0.10/src/emerald_hws.egg-info/PKG-INFO +0 -18
- emerald_hws-0.0.10/src/emerald_hws.egg-info/requires.txt +0 -2
- {emerald_hws-0.0.10 → emerald_hws-0.0.12}/LICENSE +0 -0
- {emerald_hws-0.0.10 → emerald_hws-0.0.12}/setup.cfg +0 -0
- {emerald_hws-0.0.10 → emerald_hws-0.0.12}/src/emerald_hws/__assets__/SFSRootCAG2.pem +0 -0
- {emerald_hws-0.0.10 → emerald_hws-0.0.12}/src/emerald_hws.egg-info/dependency_links.txt +0 -0
- {emerald_hws-0.0.10 → emerald_hws-0.0.12}/src/emerald_hws.egg-info/top_level.txt +0 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
version: 2
|
2
|
+
updates:
|
3
|
+
- package-ecosystem: "github-actions"
|
4
|
+
directory: "/"
|
5
|
+
schedule:
|
6
|
+
interval: "weekly"
|
7
|
+
open-pull-requests-limit: 5
|
8
|
+
|
9
|
+
- package-ecosystem: "pip"
|
10
|
+
directory: "/"
|
11
|
+
schedule:
|
12
|
+
interval: "weekly"
|
13
|
+
open-pull-requests-limit: 5
|
@@ -0,0 +1,30 @@
|
|
1
|
+
name: "Lint"
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- "main"
|
7
|
+
pull_request:
|
8
|
+
branches:
|
9
|
+
- "main"
|
10
|
+
workflow_call: # Allow this workflow to be called by other workflows
|
11
|
+
|
12
|
+
jobs:
|
13
|
+
ruff:
|
14
|
+
name: "Ruff"
|
15
|
+
runs-on: "ubuntu-latest"
|
16
|
+
steps:
|
17
|
+
- name: "Checkout the repository"
|
18
|
+
uses: "actions/checkout@v4.2.2"
|
19
|
+
|
20
|
+
- name: "Set up Python"
|
21
|
+
uses: actions/setup-python@v5.6.0
|
22
|
+
with:
|
23
|
+
python-version: "3.11"
|
24
|
+
cache: "pip"
|
25
|
+
|
26
|
+
- name: "Install requirements"
|
27
|
+
run: python3 -m pip install -e ".[dev]"
|
28
|
+
|
29
|
+
- name: "Run"
|
30
|
+
run: python3 -m ruff check .
|
@@ -0,0 +1,222 @@
|
|
1
|
+
name: "Publish to PyPI"
|
2
|
+
|
3
|
+
on:
|
4
|
+
release:
|
5
|
+
types: [published]
|
6
|
+
workflow_dispatch:
|
7
|
+
inputs:
|
8
|
+
test_pypi:
|
9
|
+
description: "Publish to Test PyPI instead of PyPI"
|
10
|
+
required: false
|
11
|
+
default: false
|
12
|
+
type: boolean
|
13
|
+
|
14
|
+
jobs:
|
15
|
+
# Security validation (defense in depth - primary security is OIDC)
|
16
|
+
security-check:
|
17
|
+
name: "Security Validation"
|
18
|
+
runs-on: ubuntu-latest
|
19
|
+
outputs:
|
20
|
+
is-authorized: ${{ steps.check.outputs.authorized }}
|
21
|
+
steps:
|
22
|
+
- name: "Validate repository and context"
|
23
|
+
id: check
|
24
|
+
run: |
|
25
|
+
echo "🔒 Security validation:"
|
26
|
+
echo "Repository: ${{ github.repository }}"
|
27
|
+
echo "Actor: ${{ github.actor }}"
|
28
|
+
echo "Event: ${{ github.event_name }}"
|
29
|
+
echo "Ref: ${{ github.ref }}"
|
30
|
+
echo ""
|
31
|
+
|
32
|
+
# Check repository (defense in depth - OIDC is primary security)
|
33
|
+
if [[ "${{ github.repository }}" == "ross-w/emerald_hws_py" ]]; then
|
34
|
+
echo "✅ Repository validation passed"
|
35
|
+
echo "authorized=true" >> $GITHUB_OUTPUT
|
36
|
+
else
|
37
|
+
echo "❌ Repository validation failed"
|
38
|
+
echo "Expected: ross-w/emerald_hws_py"
|
39
|
+
echo "Actual: ${{ github.repository }}"
|
40
|
+
echo "authorized=false" >> $GITHUB_OUTPUT
|
41
|
+
echo ""
|
42
|
+
echo "ℹ️ Note: Even if this check was bypassed, PyPI trusted publishing"
|
43
|
+
echo " would reject the upload due to OIDC repository mismatch."
|
44
|
+
fi
|
45
|
+
|
46
|
+
lint:
|
47
|
+
name: "Lint Check"
|
48
|
+
needs: security-check
|
49
|
+
if: needs.security-check.outputs.is-authorized == 'true'
|
50
|
+
uses: ./.github/workflows/lint.yml
|
51
|
+
|
52
|
+
smoke-test:
|
53
|
+
name: "Smoke Test"
|
54
|
+
needs: security-check
|
55
|
+
if: needs.security-check.outputs.is-authorized == 'true'
|
56
|
+
uses: ./.github/workflows/smoke-test.yml
|
57
|
+
|
58
|
+
# Additional publish-specific validation
|
59
|
+
version-check:
|
60
|
+
name: "Version Validation"
|
61
|
+
needs: security-check
|
62
|
+
if: needs.security-check.outputs.is-authorized == 'true'
|
63
|
+
runs-on: ubuntu-latest
|
64
|
+
|
65
|
+
steps:
|
66
|
+
- name: "Checkout repository"
|
67
|
+
uses: "actions/checkout@v4"
|
68
|
+
with:
|
69
|
+
# Fetch full history for setuptools-scm version detection
|
70
|
+
fetch-depth: 0
|
71
|
+
|
72
|
+
- name: "Set up Python"
|
73
|
+
uses: "actions/setup-python@v5"
|
74
|
+
with:
|
75
|
+
python-version: "3.11"
|
76
|
+
cache: "pip"
|
77
|
+
|
78
|
+
- name: "Install package"
|
79
|
+
run: |
|
80
|
+
python -m pip install --upgrade pip
|
81
|
+
python -m pip install -e .
|
82
|
+
|
83
|
+
- name: "Verify dynamic versioning for release"
|
84
|
+
run: |
|
85
|
+
python -c "
|
86
|
+
import emerald_hws
|
87
|
+
from importlib.metadata import version
|
88
|
+
pkg_version = version('emerald_hws')
|
89
|
+
print(f'📦 Detected version: {pkg_version}')
|
90
|
+
|
91
|
+
# For releases, ensure we have a proper version
|
92
|
+
if '${{ github.event_name }}' == 'release':
|
93
|
+
if 'dev' in pkg_version or '+' in pkg_version:
|
94
|
+
print('❌ Development version detected for release')
|
95
|
+
print(' This suggests the release tag may not be properly formatted')
|
96
|
+
exit(1)
|
97
|
+
else:
|
98
|
+
print('✅ Release version detected')
|
99
|
+
else:
|
100
|
+
print('ℹ️ Manual trigger - version validation skipped')
|
101
|
+
"
|
102
|
+
|
103
|
+
# Build package
|
104
|
+
build:
|
105
|
+
name: "Build Package"
|
106
|
+
needs: [security-check, lint, smoke-test, version-check]
|
107
|
+
if: needs.security-check.outputs.is-authorized == 'true'
|
108
|
+
runs-on: ubuntu-latest
|
109
|
+
|
110
|
+
steps:
|
111
|
+
- name: "Checkout repository"
|
112
|
+
uses: "actions/checkout@v4"
|
113
|
+
with:
|
114
|
+
fetch-depth: 0
|
115
|
+
|
116
|
+
- name: "Set up Python"
|
117
|
+
uses: "actions/setup-python@v5"
|
118
|
+
with:
|
119
|
+
python-version: "3.11"
|
120
|
+
cache: "pip"
|
121
|
+
|
122
|
+
- name: "Install build tools"
|
123
|
+
run: |
|
124
|
+
python -m pip install --upgrade pip
|
125
|
+
python -m pip install "packaging>=24.2"
|
126
|
+
python -m pip install build twine
|
127
|
+
|
128
|
+
- name: "Build distribution packages"
|
129
|
+
run: python -m build
|
130
|
+
|
131
|
+
- name: "Validate package"
|
132
|
+
run: |
|
133
|
+
echo "📦 Built packages:"
|
134
|
+
ls -la dist/
|
135
|
+
echo ""
|
136
|
+
echo "🔍 Package validation:"
|
137
|
+
python -m twine check dist/*
|
138
|
+
echo ""
|
139
|
+
echo "📋 Package metadata:"
|
140
|
+
python -m pip install pkginfo
|
141
|
+
python -c "
|
142
|
+
import pkginfo
|
143
|
+
import glob
|
144
|
+
|
145
|
+
for wheel in glob.glob('dist/*.whl'):
|
146
|
+
info = pkginfo.get_metadata(wheel)
|
147
|
+
print(f'Name: {info.name}')
|
148
|
+
print(f'Version: {info.version}')
|
149
|
+
print(f'Author: {info.author}')
|
150
|
+
break
|
151
|
+
"
|
152
|
+
|
153
|
+
- name: "Upload build artifacts"
|
154
|
+
uses: actions/upload-artifact@v4
|
155
|
+
with:
|
156
|
+
name: dist-packages
|
157
|
+
path: dist/
|
158
|
+
retention-days: 7
|
159
|
+
|
160
|
+
# Publish using PyPI Trusted Publishing (OIDC)
|
161
|
+
publish:
|
162
|
+
name: "Publish to PyPI"
|
163
|
+
needs: [security-check, lint, smoke-test, version-check, build]
|
164
|
+
if: needs.security-check.outputs.is-authorized == 'true'
|
165
|
+
runs-on: ubuntu-latest
|
166
|
+
|
167
|
+
# CRITICAL SECURITY: This environment provides additional protection
|
168
|
+
# beyond OIDC trusted publishing. Forks cannot access this environment.
|
169
|
+
environment:
|
170
|
+
name: pypi
|
171
|
+
url: https://pypi.org/p/emerald-hws
|
172
|
+
|
173
|
+
# CRITICAL SECURITY: These permissions enable OIDC trusted publishing
|
174
|
+
# The id-token is cryptographically tied to this specific repository
|
175
|
+
permissions:
|
176
|
+
id-token: write # Required for PyPI trusted publishing
|
177
|
+
contents: read # Required to download artifacts
|
178
|
+
|
179
|
+
steps:
|
180
|
+
- name: "Download build artifacts"
|
181
|
+
uses: actions/download-artifact@v4
|
182
|
+
with:
|
183
|
+
name: dist-packages
|
184
|
+
path: dist/
|
185
|
+
|
186
|
+
- name: "Final security and package validation"
|
187
|
+
run: |
|
188
|
+
echo "🔒 Final security context:"
|
189
|
+
echo "Repository: ${{ github.repository }}"
|
190
|
+
echo "Environment: pypi"
|
191
|
+
echo "OIDC Token: Will be generated for this specific repository"
|
192
|
+
echo ""
|
193
|
+
echo "📦 Final package validation:"
|
194
|
+
ls -la dist/
|
195
|
+
python -m pip install "packaging>=24.2"
|
196
|
+
python -m pip install twine
|
197
|
+
python -m twine check dist/*
|
198
|
+
|
199
|
+
- name: "Publish to Test PyPI"
|
200
|
+
if: github.event.inputs.test_pypi == 'true'
|
201
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
202
|
+
with:
|
203
|
+
repository-url: https://test.pypi.org/legacy/
|
204
|
+
print-hash: true
|
205
|
+
|
206
|
+
- name: "Publish to PyPI"
|
207
|
+
if: github.event.inputs.test_pypi != 'true'
|
208
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
209
|
+
with:
|
210
|
+
print-hash: true
|
211
|
+
|
212
|
+
- name: "Publication summary"
|
213
|
+
run: |
|
214
|
+
if [[ "${{ github.event.inputs.test_pypi }}" == "true" ]]; then
|
215
|
+
echo "✅ Package successfully published to Test PyPI"
|
216
|
+
echo "🔗 View at: https://test.pypi.org/project/emerald-hws/"
|
217
|
+
else
|
218
|
+
echo "✅ Package successfully published to PyPI"
|
219
|
+
echo "🔗 View at: https://pypi.org/project/emerald-hws/"
|
220
|
+
fi
|
221
|
+
echo "📦 Published from release: ${{ github.ref_name }}"
|
222
|
+
echo "🔒 Security: Verified via PyPI trusted publishing (OIDC)"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
name: "Smoke Test"
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- "main"
|
7
|
+
pull_request:
|
8
|
+
branches:
|
9
|
+
- "main"
|
10
|
+
workflow_call: # Allow this workflow to be called by other workflows
|
11
|
+
|
12
|
+
jobs:
|
13
|
+
smoke-test:
|
14
|
+
name: "Import Test"
|
15
|
+
runs-on: "ubuntu-latest"
|
16
|
+
strategy:
|
17
|
+
matrix:
|
18
|
+
python-version: ["3.11", "3.12", "3.13"]
|
19
|
+
|
20
|
+
steps:
|
21
|
+
- name: "Checkout repository"
|
22
|
+
uses: "actions/checkout@v4"
|
23
|
+
|
24
|
+
- name: "Set up Python ${{ matrix.python-version }}"
|
25
|
+
uses: "actions/setup-python@v5"
|
26
|
+
with:
|
27
|
+
python-version: ${{ matrix.python-version }}
|
28
|
+
cache: "pip"
|
29
|
+
|
30
|
+
- name: "Install package"
|
31
|
+
run: python -m pip install -e .
|
32
|
+
|
33
|
+
- name: "Test import"
|
34
|
+
run: python -c "import emerald_hws; print('✓ emerald_hws imported successfully')"
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
2
|
+
__pycache__/
|
3
|
+
*.py[cod]
|
4
|
+
*$py.class
|
5
|
+
|
6
|
+
# C extensions
|
7
|
+
*.so
|
8
|
+
|
9
|
+
# Distribution / packaging
|
10
|
+
.Python
|
11
|
+
build/
|
12
|
+
develop-eggs/
|
13
|
+
dist/
|
14
|
+
downloads/
|
15
|
+
eggs/
|
16
|
+
.eggs/
|
17
|
+
lib/
|
18
|
+
lib64/
|
19
|
+
parts/
|
20
|
+
sdist/
|
21
|
+
var/
|
22
|
+
wheels/
|
23
|
+
share/python-wheels/
|
24
|
+
*.egg-info/
|
25
|
+
.installed.cfg
|
26
|
+
*.egg
|
27
|
+
MANIFEST
|
28
|
+
|
29
|
+
# PyInstaller
|
30
|
+
# Usually these files are written by a python script from a template
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32
|
+
*.manifest
|
33
|
+
*.spec
|
34
|
+
|
35
|
+
# Installer logs
|
36
|
+
pip-log.txt
|
37
|
+
pip-delete-this-directory.txt
|
38
|
+
|
39
|
+
# Unit test / coverage reports
|
40
|
+
htmlcov/
|
41
|
+
.tox/
|
42
|
+
.nox/
|
43
|
+
.coverage
|
44
|
+
.coverage.*
|
45
|
+
.cache
|
46
|
+
nosetests.xml
|
47
|
+
coverage.xml
|
48
|
+
*.cover
|
49
|
+
*.py,cover
|
50
|
+
.hypothesis/
|
51
|
+
.pytest_cache/
|
52
|
+
cover/
|
53
|
+
|
54
|
+
# Translations
|
55
|
+
*.mo
|
56
|
+
*.pot
|
57
|
+
|
58
|
+
# Django stuff:
|
59
|
+
*.log
|
60
|
+
local_settings.py
|
61
|
+
db.sqlite3
|
62
|
+
db.sqlite3-journal
|
63
|
+
|
64
|
+
# Flask stuff:
|
65
|
+
instance/
|
66
|
+
.webassets-cache
|
67
|
+
|
68
|
+
# Scrapy stuff:
|
69
|
+
.scrapy
|
70
|
+
|
71
|
+
# Sphinx documentation
|
72
|
+
docs/_build/
|
73
|
+
|
74
|
+
# PyBuilder
|
75
|
+
.pybuilder/
|
76
|
+
target/
|
77
|
+
|
78
|
+
# Jupyter Notebook
|
79
|
+
.ipynb_checkpoints
|
80
|
+
|
81
|
+
# IPython
|
82
|
+
profile_default/
|
83
|
+
ipython_config.py
|
84
|
+
|
85
|
+
# pyenv
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
88
|
+
# .python-version
|
89
|
+
|
90
|
+
# pipenv
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94
|
+
# install all needed dependencies.
|
95
|
+
#Pipfile.lock
|
96
|
+
|
97
|
+
# poetry
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100
|
+
# commonly ignored for libraries.
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
102
|
+
#poetry.lock
|
103
|
+
|
104
|
+
# pdm
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
106
|
+
#pdm.lock
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
108
|
+
# in version control.
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
110
|
+
.pdm.toml
|
111
|
+
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
113
|
+
__pypackages__/
|
114
|
+
|
115
|
+
# Celery stuff
|
116
|
+
celerybeat-schedule
|
117
|
+
celerybeat.pid
|
118
|
+
|
119
|
+
# SageMath parsed files
|
120
|
+
*.sage.py
|
121
|
+
|
122
|
+
# Environments
|
123
|
+
.env
|
124
|
+
.venv
|
125
|
+
env/
|
126
|
+
venv/
|
127
|
+
ENV/
|
128
|
+
env.bak/
|
129
|
+
venv.bak/
|
130
|
+
|
131
|
+
# Spyder project settings
|
132
|
+
.spyderproject
|
133
|
+
.spyproject
|
134
|
+
|
135
|
+
# Rope project settings
|
136
|
+
.ropeproject
|
137
|
+
|
138
|
+
# mkdocs documentation
|
139
|
+
/site
|
140
|
+
|
141
|
+
# mypy
|
142
|
+
.mypy_cache/
|
143
|
+
.dmypy.json
|
144
|
+
dmypy.json
|
145
|
+
|
146
|
+
# Pyre type checker
|
147
|
+
.pyre/
|
148
|
+
|
149
|
+
# pytype static type analyzer
|
150
|
+
.pytype/
|
151
|
+
|
152
|
+
# Cython debug symbols
|
153
|
+
cython_debug/
|
154
|
+
|
155
|
+
# PyCharm
|
156
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
157
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
158
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
159
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
160
|
+
#.idea/
|
@@ -0,0 +1,80 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: emerald_hws
|
3
|
+
Version: 0.0.12
|
4
|
+
Summary: A package to manipulate and monitor Emerald Heat Pump Hot Water Systems
|
5
|
+
Author-email: Ross Williamson <ross@inertia.net.nz>
|
6
|
+
License-Expression: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/ross-w/emerald_hws_py
|
8
|
+
Project-URL: Bug Tracker, https://github.com/ross-w/emerald_hws_py/issues
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.7
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: boto3<2.0.0,>=1.40.0
|
14
|
+
Requires-Dist: awsiotsdk<2.0.0,>=1.24.0
|
15
|
+
Requires-Dist: requests>=2.25.0
|
16
|
+
Provides-Extra: dev
|
17
|
+
Requires-Dist: ruff<1.0.0,>=0.12.0; extra == "dev"
|
18
|
+
|
19
|
+
# emerald_hws_py
|
20
|
+
Python package for controlling Emerald Heat Pump Hot Water Systems
|
21
|
+
|
22
|
+
## Overview
|
23
|
+
This package provides an interface to control and monitor Emerald Heat Pump Hot Water Systems through their API and MQTT service.
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
```bash
|
27
|
+
pip install emerald_hws
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
```python
|
32
|
+
from emerald_hws.emeraldhws import EmeraldHWS
|
33
|
+
|
34
|
+
# Basic usage with default connection settings
|
35
|
+
client = EmeraldHWS("your_email@example.com", "your_password")
|
36
|
+
client.connect()
|
37
|
+
|
38
|
+
# List all hot water systems
|
39
|
+
hws_list = client.listHWS()
|
40
|
+
print(f"Found {len(hws_list)} hot water systems")
|
41
|
+
|
42
|
+
# Get status of first HWS
|
43
|
+
hws_id = hws_list[0]
|
44
|
+
status = client.getFullStatus(hws_id)
|
45
|
+
print(f"Current temperature: {status['last_state'].get('temp_current')}")
|
46
|
+
|
47
|
+
# Turn on the hot water system
|
48
|
+
client.turnOn(hws_id)
|
49
|
+
```
|
50
|
+
|
51
|
+
## Configuration Options
|
52
|
+
|
53
|
+
### Connection Timeout
|
54
|
+
The module will automatically reconnect to the MQTT service periodically to prevent stale connections. You can configure this timeout:
|
55
|
+
|
56
|
+
```python
|
57
|
+
# Set connection timeout to 6 hours (360 minutes)
|
58
|
+
client = EmeraldHWS("your_email@example.com", "your_password", connection_timeout_minutes=360)
|
59
|
+
```
|
60
|
+
|
61
|
+
### Health Check
|
62
|
+
The module can proactively check for message activity and reconnect if no messages have been received for a specified period:
|
63
|
+
|
64
|
+
```python
|
65
|
+
# Set health check to check every 30 minutes
|
66
|
+
client = EmeraldHWS("your_email@example.com", "your_password", health_check_minutes=30)
|
67
|
+
|
68
|
+
# Disable health check
|
69
|
+
client = EmeraldHWS("your_email@example.com", "your_password", health_check_minutes=0)
|
70
|
+
```
|
71
|
+
|
72
|
+
## Callback for Updates
|
73
|
+
You can register a callback function to be notified when the state of any hot water system changes:
|
74
|
+
|
75
|
+
```python
|
76
|
+
def my_callback():
|
77
|
+
print("Hot water system state updated!")
|
78
|
+
|
79
|
+
client = EmeraldHWS("your_email@example.com", "your_password", update_callback=my_callback)
|
80
|
+
```
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# emerald_hws_py
|
2
|
+
Python package for controlling Emerald Heat Pump Hot Water Systems
|
3
|
+
|
4
|
+
## Overview
|
5
|
+
This package provides an interface to control and monitor Emerald Heat Pump Hot Water Systems through their API and MQTT service.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
```bash
|
9
|
+
pip install emerald_hws
|
10
|
+
```
|
11
|
+
|
12
|
+
## Usage
|
13
|
+
```python
|
14
|
+
from emerald_hws.emeraldhws import EmeraldHWS
|
15
|
+
|
16
|
+
# Basic usage with default connection settings
|
17
|
+
client = EmeraldHWS("your_email@example.com", "your_password")
|
18
|
+
client.connect()
|
19
|
+
|
20
|
+
# List all hot water systems
|
21
|
+
hws_list = client.listHWS()
|
22
|
+
print(f"Found {len(hws_list)} hot water systems")
|
23
|
+
|
24
|
+
# Get status of first HWS
|
25
|
+
hws_id = hws_list[0]
|
26
|
+
status = client.getFullStatus(hws_id)
|
27
|
+
print(f"Current temperature: {status['last_state'].get('temp_current')}")
|
28
|
+
|
29
|
+
# Turn on the hot water system
|
30
|
+
client.turnOn(hws_id)
|
31
|
+
```
|
32
|
+
|
33
|
+
## Configuration Options
|
34
|
+
|
35
|
+
### Connection Timeout
|
36
|
+
The module will automatically reconnect to the MQTT service periodically to prevent stale connections. You can configure this timeout:
|
37
|
+
|
38
|
+
```python
|
39
|
+
# Set connection timeout to 6 hours (360 minutes)
|
40
|
+
client = EmeraldHWS("your_email@example.com", "your_password", connection_timeout_minutes=360)
|
41
|
+
```
|
42
|
+
|
43
|
+
### Health Check
|
44
|
+
The module can proactively check for message activity and reconnect if no messages have been received for a specified period:
|
45
|
+
|
46
|
+
```python
|
47
|
+
# Set health check to check every 30 minutes
|
48
|
+
client = EmeraldHWS("your_email@example.com", "your_password", health_check_minutes=30)
|
49
|
+
|
50
|
+
# Disable health check
|
51
|
+
client = EmeraldHWS("your_email@example.com", "your_password", health_check_minutes=0)
|
52
|
+
```
|
53
|
+
|
54
|
+
## Callback for Updates
|
55
|
+
You can register a callback function to be notified when the state of any hot water system changes:
|
56
|
+
|
57
|
+
```python
|
58
|
+
def my_callback():
|
59
|
+
print("Hot water system state updated!")
|
60
|
+
|
61
|
+
client = EmeraldHWS("your_email@example.com", "your_password", update_callback=my_callback)
|
62
|
+
```
|
@@ -0,0 +1,65 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools>=70.0", "setuptools-scm>=8.0"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "emerald_hws"
|
7
|
+
dynamic = ["version"]
|
8
|
+
license = "MIT"
|
9
|
+
dependencies = [
|
10
|
+
"boto3>=1.40.0,<2.0.0",
|
11
|
+
"awsiotsdk>=1.24.0,<2.0.0",
|
12
|
+
"requests>=2.25.0"
|
13
|
+
]
|
14
|
+
authors = [{ name = "Ross Williamson", email = "ross@inertia.net.nz" }]
|
15
|
+
description = "A package to manipulate and monitor Emerald Heat Pump Hot Water Systems"
|
16
|
+
readme = "README.md"
|
17
|
+
requires-python = ">=3.7"
|
18
|
+
classifiers = [
|
19
|
+
"Programming Language :: Python :: 3",
|
20
|
+
"Operating System :: OS Independent",
|
21
|
+
]
|
22
|
+
|
23
|
+
[project.optional-dependencies]
|
24
|
+
dev = [
|
25
|
+
"ruff>=0.12.0,<1.0.0"
|
26
|
+
]
|
27
|
+
|
28
|
+
[tool.setuptools]
|
29
|
+
include-package-data = true
|
30
|
+
license-files = []
|
31
|
+
|
32
|
+
[tool.setuptools.packages.find]
|
33
|
+
where = ["src"]
|
34
|
+
|
35
|
+
[tool.setuptools.package-data]
|
36
|
+
"*" = ["*.*"]
|
37
|
+
|
38
|
+
[tool.setuptools_scm]
|
39
|
+
# Use default version scheme for better compatibility
|
40
|
+
|
41
|
+
[project.urls]
|
42
|
+
"Homepage" = "https://github.com/ross-w/emerald_hws_py"
|
43
|
+
"Bug Tracker" = "https://github.com/ross-w/emerald_hws_py/issues"
|
44
|
+
|
45
|
+
[tool.ruff]
|
46
|
+
# Exclude a variety of commonly ignored directories.
|
47
|
+
exclude = [
|
48
|
+
".git",
|
49
|
+
".ruff_cache",
|
50
|
+
"__pypackages__",
|
51
|
+
"build",
|
52
|
+
"dist",
|
53
|
+
]
|
54
|
+
line-length = 88
|
55
|
+
|
56
|
+
[tool.ruff.lint]
|
57
|
+
# Enable flake8-bugbear (`B`) rules.
|
58
|
+
select = ["E", "F", "B"]
|
59
|
+
# Allow unused variables when underscore-prefixed.
|
60
|
+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
61
|
+
# Ignore specific rules
|
62
|
+
ignore = [
|
63
|
+
"E501", # Line too long (handled by formatter)
|
64
|
+
"F401", # Unused imports (some imports are used in commented code)
|
65
|
+
]
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# Dependencies are now managed in pyproject.toml
|
2
|
+
# Install with: pip install -e .
|
3
|
+
# For development: pip install -e ".[dev]"
|
4
|
+
#
|
5
|
+
# This file is kept for compatibility but pyproject.toml is the source of truth
|
6
|
+
boto3>=1.40.0,<2.0.0
|
7
|
+
awsiotsdk>=1.24.0,<2.0.0
|
8
|
+
requests>=2.25.0
|
@@ -1,12 +1,14 @@
|
|
1
1
|
import json
|
2
|
-
import requests
|
3
|
-
import os
|
4
2
|
import logging
|
5
|
-
import
|
3
|
+
import os
|
6
4
|
import random
|
7
5
|
import threading
|
8
|
-
|
9
|
-
|
6
|
+
import time
|
7
|
+
|
8
|
+
import boto3
|
9
|
+
import requests
|
10
|
+
from awscrt import mqtt5, auth, io
|
11
|
+
from awsiot import mqtt5_client_builder
|
10
12
|
|
11
13
|
|
12
14
|
class EmeraldHWS():
|
@@ -22,11 +24,13 @@ class EmeraldHWS():
|
|
22
24
|
MQTT_HOST = "a13v32g67itvz9-ats.iot.ap-southeast-2.amazonaws.com"
|
23
25
|
COGNITO_IDENTITY_POOL_ID = "ap-southeast-2:f5bbb02c-c00e-4f10-acb3-e7d1b05268e8"
|
24
26
|
|
25
|
-
def __init__(self, email, password, update_callback=None):
|
27
|
+
def __init__(self, email, password, update_callback=None, connection_timeout_minutes=720, health_check_minutes=60):
|
26
28
|
""" Initialise the API client
|
27
29
|
:param email: The email address for logging into the Emerald app
|
28
30
|
:param password: The password for the supplied user account
|
29
31
|
:param update_callback: Optional callback function to be called when an update is available
|
32
|
+
:param connection_timeout_minutes: Optional timeout in minutes before reconnecting MQTT (default: 720 minutes/12 hours)
|
33
|
+
:param health_check_minutes: Optional interval in minutes to check for message activity (default: 60 minutes/1 hour)
|
30
34
|
"""
|
31
35
|
|
32
36
|
self.email = email
|
@@ -35,6 +39,27 @@ class EmeraldHWS():
|
|
35
39
|
self.properties = {}
|
36
40
|
self.logger = logging.getLogger()
|
37
41
|
self.update_callback = update_callback
|
42
|
+
|
43
|
+
# Convert minutes to seconds for internal use
|
44
|
+
self.connection_timeout = connection_timeout_minutes * 60.0
|
45
|
+
self.health_check_interval = health_check_minutes * 60.0 if health_check_minutes > 0 else 0
|
46
|
+
self.last_message_time = None
|
47
|
+
self.health_check_timer = None
|
48
|
+
|
49
|
+
# Connection state tracking
|
50
|
+
self.connection_state = "initial" # possible states: initial, connected, failed
|
51
|
+
self.consecutive_failures = 0
|
52
|
+
self.max_backoff_seconds = 60 # Maximum backoff of 1 minute
|
53
|
+
|
54
|
+
# Ensure reasonable minimum values (e.g., at least 5 minutes for connection timeout)
|
55
|
+
if connection_timeout_minutes < 5 and connection_timeout_minutes != 0:
|
56
|
+
self.logger.warning("emeraldhws: Connection timeout too short, setting to minimum of 5 minutes")
|
57
|
+
self.connection_timeout = 5 * 60.0
|
58
|
+
|
59
|
+
# Ensure reasonable minimum values for health check (e.g., at least 5 minutes)
|
60
|
+
if 0 < health_check_minutes < 5:
|
61
|
+
self.logger.warning("emeraldhws: Health check interval too short, setting to minimum of 5 minutes")
|
62
|
+
self.health_check_interval = 5 * 60.0
|
38
63
|
|
39
64
|
def getLoginToken(self):
|
40
65
|
""" Performs an API request to get a token from the API
|
@@ -87,20 +112,48 @@ class EmeraldHWS():
|
|
87
112
|
|
88
113
|
self.update_callback = update_callback
|
89
114
|
|
90
|
-
def reconnectMQTT(self):
|
115
|
+
def reconnectMQTT(self, reason="scheduled"):
|
91
116
|
""" Stops an existing MQTT connection and creates a new one
|
117
|
+
:param reason: Reason for reconnection (scheduled, health_check, etc.)
|
92
118
|
"""
|
93
|
-
|
94
|
-
|
119
|
+
self.logger.info(f"emeraldhws: awsiot: Reconnecting MQTT connection (reason: {reason})")
|
120
|
+
|
121
|
+
# Store current temperature values for comparison after reconnect
|
122
|
+
temp_values = {}
|
123
|
+
for properties in self.properties:
|
124
|
+
heat_pumps = properties.get('heat_pump', [])
|
125
|
+
for heat_pump in heat_pumps:
|
126
|
+
hws_id = heat_pump['id']
|
127
|
+
if 'last_state' in heat_pump and 'temp_current' in heat_pump['last_state']:
|
128
|
+
temp_values[hws_id] = heat_pump['last_state']['temp_current']
|
129
|
+
|
95
130
|
self.mqttClient.stop()
|
96
131
|
self.connectMQTT()
|
97
132
|
self.subscribeAllHWS()
|
133
|
+
|
134
|
+
# After reconnection, check if temperatures have changed
|
135
|
+
def check_temp_changes():
|
136
|
+
for properties in self.properties:
|
137
|
+
heat_pumps = properties.get('heat_pump', [])
|
138
|
+
for heat_pump in heat_pumps:
|
139
|
+
hws_id = heat_pump['id']
|
140
|
+
if (hws_id in temp_values and
|
141
|
+
'last_state' in heat_pump and
|
142
|
+
'temp_current' in heat_pump['last_state']):
|
143
|
+
old_temp = temp_values[hws_id]
|
144
|
+
new_temp = heat_pump['last_state']['temp_current']
|
145
|
+
if old_temp != new_temp:
|
146
|
+
self.logger.info(f"emeraldhws: Temperature changed after reconnect for {hws_id}: {old_temp} → {new_temp}")
|
147
|
+
|
148
|
+
# Check for temperature changes after a short delay to allow for updates
|
149
|
+
threading.Timer(10.0, check_temp_changes).start()
|
98
150
|
|
99
151
|
def connectMQTT(self):
|
100
152
|
""" Establishes a connection to Amazon IOT core's MQTT service
|
101
153
|
"""
|
102
154
|
|
103
|
-
|
155
|
+
# Certificate path is available but not currently used in the connection
|
156
|
+
# os.path.join(os.path.dirname(__file__), '__assets__', 'SFSRootCAG2.pem')
|
104
157
|
identityPoolID = self.COGNITO_IDENTITY_POOL_ID
|
105
158
|
region = self.MQTT_HOST.split('.')[2]
|
106
159
|
cognito_endpoint = "cognito-identity." + region + ".amazonaws.com"
|
@@ -131,7 +184,16 @@ class EmeraldHWS():
|
|
131
184
|
|
132
185
|
client.start()
|
133
186
|
self.mqttClient = client
|
134
|
-
|
187
|
+
|
188
|
+
# Schedule periodic reconnection using configurable timeout
|
189
|
+
if self.connection_timeout > 0:
|
190
|
+
threading.Timer(self.connection_timeout, self.reconnectMQTT).start()
|
191
|
+
|
192
|
+
# Start health check timer if enabled
|
193
|
+
if self.health_check_interval > 0:
|
194
|
+
self.health_check_timer = threading.Timer(self.health_check_interval, self.check_connection_health)
|
195
|
+
self.health_check_timer.daemon = True
|
196
|
+
self.health_check_timer.start()
|
135
197
|
|
136
198
|
def mqttDecodeUpdate(self, topic, payload):
|
137
199
|
""" Attempt to decode a received MQTT message and direct appropriately
|
@@ -153,12 +215,15 @@ class EmeraldHWS():
|
|
153
215
|
publish_packet = publish_packet_data.publish_packet
|
154
216
|
assert isinstance(publish_packet, mqtt5.PublishPacket)
|
155
217
|
self.logger.debug("emeraldhws: awsiot: Received message from MQTT topic {}: {}".format(publish_packet.topic, publish_packet.payload))
|
218
|
+
self.last_message_time = time.time() # Update the last message time
|
156
219
|
self.mqttDecodeUpdate(publish_packet.topic, publish_packet.payload)
|
157
220
|
|
158
221
|
def on_connection_interrupted(self, connection, error, **kwargs):
|
159
222
|
""" Log error when MQTT is interrupted
|
160
223
|
"""
|
161
|
-
|
224
|
+
error_code = getattr(error, 'code', 'unknown')
|
225
|
+
error_name = getattr(error, 'name', 'unknown')
|
226
|
+
self.logger.info(f"emeraldhws: awsiot: Connection interrupted. Error: {error_name} (code: {error_code}), Message: {error}")
|
162
227
|
|
163
228
|
def on_connection_resumed(self, connection, return_code, session_present, **kwargs):
|
164
229
|
""" Log message when MQTT is resumed
|
@@ -169,12 +234,35 @@ class EmeraldHWS():
|
|
169
234
|
""" Log message when connection succeeded
|
170
235
|
"""
|
171
236
|
self.logger.debug("emeraldhws: awsiot: connection succeeded")
|
237
|
+
# Reset failure counter and update connection state
|
238
|
+
self.consecutive_failures = 0
|
239
|
+
self.connection_state = "connected"
|
172
240
|
return
|
173
241
|
|
174
242
|
def on_lifecycle_connection_failure(self, lifecycle_connection_failure: mqtt5.LifecycleConnectFailureData):
|
175
243
|
""" Log message when connection failed
|
176
244
|
"""
|
177
|
-
|
245
|
+
error = lifecycle_connection_failure.error
|
246
|
+
error_code = getattr(error, 'code', 'unknown')
|
247
|
+
error_name = getattr(error, 'name', 'unknown')
|
248
|
+
error_message = str(error)
|
249
|
+
|
250
|
+
# Update connection state and increment failure counter
|
251
|
+
self.connection_state = "failed"
|
252
|
+
self.consecutive_failures += 1
|
253
|
+
|
254
|
+
# Log at INFO level since this is important for troubleshooting
|
255
|
+
self.logger.info(f"emeraldhws: awsiot: connection failed - Error: {error_name} (code: {error_code}), Message: {error_message}")
|
256
|
+
|
257
|
+
# If there's a CONNACK packet available, log its details too
|
258
|
+
if hasattr(lifecycle_connection_failure, 'connack_packet') and lifecycle_connection_failure.connack_packet:
|
259
|
+
connack = lifecycle_connection_failure.connack_packet
|
260
|
+
reason_code = getattr(connack, 'reason_code', 'unknown')
|
261
|
+
reason_string = getattr(connack, 'reason_string', '')
|
262
|
+
if reason_string:
|
263
|
+
self.logger.info(f"emeraldhws: awsiot: MQTT CONNACK reason: {reason_code} - {reason_string}")
|
264
|
+
else:
|
265
|
+
self.logger.info(f"emeraldhws: awsiot: MQTT CONNACK reason code: {reason_code}")
|
178
266
|
return
|
179
267
|
|
180
268
|
def on_lifecycle_stopped(self, lifecycle_stopped_data: mqtt5.LifecycleStoppedData):
|
@@ -186,14 +274,56 @@ class EmeraldHWS():
|
|
186
274
|
def on_lifecycle_disconnection(self, lifecycle_disconnect_data: mqtt5.LifecycleDisconnectData):
|
187
275
|
""" Log message when disconnected
|
188
276
|
"""
|
189
|
-
|
277
|
+
# Extract disconnect reason if available
|
278
|
+
reason = "unknown reason"
|
279
|
+
if hasattr(lifecycle_disconnect_data, 'disconnect_packet') and lifecycle_disconnect_data.disconnect_packet:
|
280
|
+
reason_code = getattr(lifecycle_disconnect_data.disconnect_packet, 'reason_code', 'unknown')
|
281
|
+
reason_string = getattr(lifecycle_disconnect_data.disconnect_packet, 'reason_string', '')
|
282
|
+
reason = f"reason code: {reason_code}" + (f" - {reason_string}" if reason_string else "")
|
283
|
+
|
284
|
+
self.logger.info(f"emeraldhws: awsiot: disconnected - {reason}")
|
190
285
|
return
|
191
286
|
|
192
287
|
def on_lifecycle_attempting_connect(self, lifecycle_attempting_connect_data: mqtt5.LifecycleAttemptingConnectData):
|
193
288
|
""" Log message when attempting connect
|
194
289
|
"""
|
195
|
-
|
290
|
+
# Include endpoint information if available
|
291
|
+
endpoint = getattr(lifecycle_attempting_connect_data, 'endpoint', 'unknown')
|
292
|
+
self.logger.debug(f"emeraldhws: awsiot: attempting to connect to {endpoint}")
|
196
293
|
return
|
294
|
+
|
295
|
+
def check_connection_health(self):
|
296
|
+
""" Check if we've received any messages recently, reconnect if not
|
297
|
+
"""
|
298
|
+
if self.last_message_time is None:
|
299
|
+
# No messages received yet, don't reconnect
|
300
|
+
self.logger.debug("emeraldhws: awsiot: Health check - No messages received yet")
|
301
|
+
else:
|
302
|
+
current_time = time.time()
|
303
|
+
time_since_last_message = current_time - self.last_message_time
|
304
|
+
minutes_since_last = time_since_last_message / 60.0
|
305
|
+
|
306
|
+
if time_since_last_message > self.health_check_interval:
|
307
|
+
# This is an INFO level log because it's an important event
|
308
|
+
self.logger.info(f"emeraldhws: awsiot: No messages received for {minutes_since_last:.1f} minutes, reconnecting")
|
309
|
+
|
310
|
+
# If we're in a failed state, apply exponential backoff
|
311
|
+
if self.connection_state == "failed" and self.consecutive_failures > 0:
|
312
|
+
# Calculate backoff time with exponential increase, capped at max_backoff_seconds
|
313
|
+
backoff_seconds = min(2 ** (self.consecutive_failures - 1), self.max_backoff_seconds)
|
314
|
+
self.logger.info(f"emeraldhws: awsiot: Connection in failed state, applying backoff of {backoff_seconds} seconds before retry (attempt {self.consecutive_failures})")
|
315
|
+
time.sleep(backoff_seconds)
|
316
|
+
|
317
|
+
self.reconnectMQTT(reason="health_check")
|
318
|
+
else:
|
319
|
+
# This is a DEBUG level log to avoid cluttering logs
|
320
|
+
self.logger.debug(f"emeraldhws: awsiot: Health check - Last message received {minutes_since_last:.1f} minutes ago")
|
321
|
+
|
322
|
+
# Schedule next health check
|
323
|
+
if self.health_check_interval > 0:
|
324
|
+
self.health_check_timer = threading.Timer(self.health_check_interval, self.check_connection_health)
|
325
|
+
self.health_check_timer.daemon = True
|
326
|
+
self.health_check_timer.start()
|
197
327
|
|
198
328
|
def updateHWSState(self, id, key, value):
|
199
329
|
""" Updates the specified value for the supplied key in the HWS id specified
|
@@ -207,7 +337,7 @@ class EmeraldHWS():
|
|
207
337
|
for heat_pump in heat_pumps:
|
208
338
|
if heat_pump['id'] == id:
|
209
339
|
heat_pump['last_state'][key] = value
|
210
|
-
if self.update_callback
|
340
|
+
if self.update_callback is not None:
|
211
341
|
self.update_callback()
|
212
342
|
|
213
343
|
def subscribeForUpdates(self, id):
|
@@ -224,7 +354,8 @@ class EmeraldHWS():
|
|
224
354
|
topic_filter=mqtt_topic,
|
225
355
|
qos=mqtt5.QoS.AT_LEAST_ONCE)]))
|
226
356
|
|
227
|
-
|
357
|
+
# Wait for subscription to complete
|
358
|
+
subscribe_future.result(20)
|
228
359
|
|
229
360
|
def getFullStatus(self, id):
|
230
361
|
""" Returns a dict with the full status of the specified HWS
|
@@ -0,0 +1,80 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: emerald_hws
|
3
|
+
Version: 0.0.12
|
4
|
+
Summary: A package to manipulate and monitor Emerald Heat Pump Hot Water Systems
|
5
|
+
Author-email: Ross Williamson <ross@inertia.net.nz>
|
6
|
+
License-Expression: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/ross-w/emerald_hws_py
|
8
|
+
Project-URL: Bug Tracker, https://github.com/ross-w/emerald_hws_py/issues
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.7
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: boto3<2.0.0,>=1.40.0
|
14
|
+
Requires-Dist: awsiotsdk<2.0.0,>=1.24.0
|
15
|
+
Requires-Dist: requests>=2.25.0
|
16
|
+
Provides-Extra: dev
|
17
|
+
Requires-Dist: ruff<1.0.0,>=0.12.0; extra == "dev"
|
18
|
+
|
19
|
+
# emerald_hws_py
|
20
|
+
Python package for controlling Emerald Heat Pump Hot Water Systems
|
21
|
+
|
22
|
+
## Overview
|
23
|
+
This package provides an interface to control and monitor Emerald Heat Pump Hot Water Systems through their API and MQTT service.
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
```bash
|
27
|
+
pip install emerald_hws
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
```python
|
32
|
+
from emerald_hws.emeraldhws import EmeraldHWS
|
33
|
+
|
34
|
+
# Basic usage with default connection settings
|
35
|
+
client = EmeraldHWS("your_email@example.com", "your_password")
|
36
|
+
client.connect()
|
37
|
+
|
38
|
+
# List all hot water systems
|
39
|
+
hws_list = client.listHWS()
|
40
|
+
print(f"Found {len(hws_list)} hot water systems")
|
41
|
+
|
42
|
+
# Get status of first HWS
|
43
|
+
hws_id = hws_list[0]
|
44
|
+
status = client.getFullStatus(hws_id)
|
45
|
+
print(f"Current temperature: {status['last_state'].get('temp_current')}")
|
46
|
+
|
47
|
+
# Turn on the hot water system
|
48
|
+
client.turnOn(hws_id)
|
49
|
+
```
|
50
|
+
|
51
|
+
## Configuration Options
|
52
|
+
|
53
|
+
### Connection Timeout
|
54
|
+
The module will automatically reconnect to the MQTT service periodically to prevent stale connections. You can configure this timeout:
|
55
|
+
|
56
|
+
```python
|
57
|
+
# Set connection timeout to 6 hours (360 minutes)
|
58
|
+
client = EmeraldHWS("your_email@example.com", "your_password", connection_timeout_minutes=360)
|
59
|
+
```
|
60
|
+
|
61
|
+
### Health Check
|
62
|
+
The module can proactively check for message activity and reconnect if no messages have been received for a specified period:
|
63
|
+
|
64
|
+
```python
|
65
|
+
# Set health check to check every 30 minutes
|
66
|
+
client = EmeraldHWS("your_email@example.com", "your_password", health_check_minutes=30)
|
67
|
+
|
68
|
+
# Disable health check
|
69
|
+
client = EmeraldHWS("your_email@example.com", "your_password", health_check_minutes=0)
|
70
|
+
```
|
71
|
+
|
72
|
+
## Callback for Updates
|
73
|
+
You can register a callback function to be notified when the state of any hot water system changes:
|
74
|
+
|
75
|
+
```python
|
76
|
+
def my_callback():
|
77
|
+
print("Hot water system state updated!")
|
78
|
+
|
79
|
+
client = EmeraldHWS("your_email@example.com", "your_password", update_callback=my_callback)
|
80
|
+
```
|
@@ -1,6 +1,12 @@
|
|
1
|
+
.gitignore
|
1
2
|
LICENSE
|
2
3
|
README.md
|
3
4
|
pyproject.toml
|
5
|
+
requirements.txt
|
6
|
+
.github/dependabot.yml
|
7
|
+
.github/workflows/lint.yml
|
8
|
+
.github/workflows/publish.yml
|
9
|
+
.github/workflows/smoke-test.yml
|
4
10
|
src/emerald_hws/__init__.py
|
5
11
|
src/emerald_hws/emeraldhws.py
|
6
12
|
src/emerald_hws.egg-info/PKG-INFO
|
emerald_hws-0.0.10/PKG-INFO
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.2
|
2
|
-
Name: emerald_hws
|
3
|
-
Version: 0.0.10
|
4
|
-
Summary: A package to manipulate and monitor Emerald Heat Pump Hot Water Systems
|
5
|
-
Author-email: Ross Williamson <ross@inertia.net.nz>
|
6
|
-
Project-URL: Homepage, https://github.com/ross-w/emerald_hws_py
|
7
|
-
Project-URL: Bug Tracker, https://github.com/ross-w/emerald_hws_py/issues
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
|
-
Classifier: Operating System :: OS Independent
|
11
|
-
Requires-Python: >=3.7
|
12
|
-
Description-Content-Type: text/markdown
|
13
|
-
License-File: LICENSE
|
14
|
-
Requires-Dist: boto3
|
15
|
-
Requires-Dist: awsiotsdk
|
16
|
-
|
17
|
-
# emerald_hws_py
|
18
|
-
Python package for controlling Emerald Heat Pump Hot Water Systems
|
emerald_hws-0.0.10/README.md
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
[build-system]
|
2
|
-
requires = ["setuptools>=61.0"]
|
3
|
-
build-backend = "setuptools.build_meta"
|
4
|
-
|
5
|
-
[project]
|
6
|
-
name = "emerald_hws"
|
7
|
-
version = "0.0.10"
|
8
|
-
dependencies = ["boto3", "awsiotsdk"]
|
9
|
-
authors = [{ name = "Ross Williamson", email = "ross@inertia.net.nz" }]
|
10
|
-
description = "A package to manipulate and monitor Emerald Heat Pump Hot Water Systems"
|
11
|
-
readme = "README.md"
|
12
|
-
requires-python = ">=3.7"
|
13
|
-
classifiers = [
|
14
|
-
"Programming Language :: Python :: 3",
|
15
|
-
"License :: OSI Approved :: MIT License",
|
16
|
-
"Operating System :: OS Independent",
|
17
|
-
]
|
18
|
-
|
19
|
-
[tool.setuptools]
|
20
|
-
include-package-data = true
|
21
|
-
|
22
|
-
[tool.setuptools.packages.find]
|
23
|
-
where = ["src"]
|
24
|
-
|
25
|
-
[tool.setuptools.package-data]
|
26
|
-
"*" = ["*.*"]
|
27
|
-
|
28
|
-
[project.urls]
|
29
|
-
"Homepage" = "https://github.com/ross-w/emerald_hws_py"
|
30
|
-
"Bug Tracker" = "https://github.com/ross-w/emerald_hws_py/issues"
|
@@ -1 +0,0 @@
|
|
1
|
-
|
@@ -1,18 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.2
|
2
|
-
Name: emerald_hws
|
3
|
-
Version: 0.0.10
|
4
|
-
Summary: A package to manipulate and monitor Emerald Heat Pump Hot Water Systems
|
5
|
-
Author-email: Ross Williamson <ross@inertia.net.nz>
|
6
|
-
Project-URL: Homepage, https://github.com/ross-w/emerald_hws_py
|
7
|
-
Project-URL: Bug Tracker, https://github.com/ross-w/emerald_hws_py/issues
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
|
-
Classifier: Operating System :: OS Independent
|
11
|
-
Requires-Python: >=3.7
|
12
|
-
Description-Content-Type: text/markdown
|
13
|
-
License-File: LICENSE
|
14
|
-
Requires-Dist: boto3
|
15
|
-
Requires-Dist: awsiotsdk
|
16
|
-
|
17
|
-
# emerald_hws_py
|
18
|
-
Python package for controlling Emerald Heat Pump Hot Water Systems
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|