emerald-hws 0.0.11__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.11/src/emerald_hws.egg-info → emerald_hws-0.0.12}/PKG-INFO +7 -6
- 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.11 → emerald_hws-0.0.12}/src/emerald_hws/emeraldhws.py +62 -12
- {emerald_hws-0.0.11 → emerald_hws-0.0.12/src/emerald_hws.egg-info}/PKG-INFO +7 -6
- {emerald_hws-0.0.11 → 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.11/pyproject.toml +0 -30
- emerald_hws-0.0.11/src/emerald_hws/__init__.py +0 -1
- emerald_hws-0.0.11/src/emerald_hws.egg-info/requires.txt +0 -2
- {emerald_hws-0.0.11 → emerald_hws-0.0.12}/LICENSE +0 -0
- {emerald_hws-0.0.11 → emerald_hws-0.0.12}/README.md +0 -0
- {emerald_hws-0.0.11 → emerald_hws-0.0.12}/setup.cfg +0 -0
- {emerald_hws-0.0.11 → emerald_hws-0.0.12}/src/emerald_hws/__assets__/SFSRootCAG2.pem +0 -0
- {emerald_hws-0.0.11 → emerald_hws-0.0.12}/src/emerald_hws.egg-info/dependency_links.txt +0 -0
- {emerald_hws-0.0.11 → 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/
|
@@ -1,19 +1,20 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: emerald_hws
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.12
|
4
4
|
Summary: A package to manipulate and monitor Emerald Heat Pump Hot Water Systems
|
5
5
|
Author-email: Ross Williamson <ross@inertia.net.nz>
|
6
|
+
License-Expression: MIT
|
6
7
|
Project-URL: Homepage, https://github.com/ross-w/emerald_hws_py
|
7
8
|
Project-URL: Bug Tracker, https://github.com/ross-w/emerald_hws_py/issues
|
8
9
|
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
10
|
Classifier: Operating System :: OS Independent
|
11
11
|
Requires-Python: >=3.7
|
12
12
|
Description-Content-Type: text/markdown
|
13
|
-
|
14
|
-
Requires-Dist:
|
15
|
-
Requires-Dist:
|
16
|
-
|
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"
|
17
18
|
|
18
19
|
# emerald_hws_py
|
19
20
|
Python package for controlling Emerald Heat Pump Hot Water Systems
|
@@ -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,13 +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
6
|
import time
|
9
|
-
|
10
|
-
|
7
|
+
|
8
|
+
import boto3
|
9
|
+
import requests
|
10
|
+
from awscrt import mqtt5, auth, io
|
11
|
+
from awsiot import mqtt5_client_builder
|
11
12
|
|
12
13
|
|
13
14
|
class EmeraldHWS():
|
@@ -45,6 +46,11 @@ class EmeraldHWS():
|
|
45
46
|
self.last_message_time = None
|
46
47
|
self.health_check_timer = None
|
47
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
|
+
|
48
54
|
# Ensure reasonable minimum values (e.g., at least 5 minutes for connection timeout)
|
49
55
|
if connection_timeout_minutes < 5 and connection_timeout_minutes != 0:
|
50
56
|
self.logger.warning("emeraldhws: Connection timeout too short, setting to minimum of 5 minutes")
|
@@ -146,7 +152,8 @@ class EmeraldHWS():
|
|
146
152
|
""" Establishes a connection to Amazon IOT core's MQTT service
|
147
153
|
"""
|
148
154
|
|
149
|
-
|
155
|
+
# Certificate path is available but not currently used in the connection
|
156
|
+
# os.path.join(os.path.dirname(__file__), '__assets__', 'SFSRootCAG2.pem')
|
150
157
|
identityPoolID = self.COGNITO_IDENTITY_POOL_ID
|
151
158
|
region = self.MQTT_HOST.split('.')[2]
|
152
159
|
cognito_endpoint = "cognito-identity." + region + ".amazonaws.com"
|
@@ -214,7 +221,9 @@ class EmeraldHWS():
|
|
214
221
|
def on_connection_interrupted(self, connection, error, **kwargs):
|
215
222
|
""" Log error when MQTT is interrupted
|
216
223
|
"""
|
217
|
-
|
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}")
|
218
227
|
|
219
228
|
def on_connection_resumed(self, connection, return_code, session_present, **kwargs):
|
220
229
|
""" Log message when MQTT is resumed
|
@@ -225,12 +234,35 @@ class EmeraldHWS():
|
|
225
234
|
""" Log message when connection succeeded
|
226
235
|
"""
|
227
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"
|
228
240
|
return
|
229
241
|
|
230
242
|
def on_lifecycle_connection_failure(self, lifecycle_connection_failure: mqtt5.LifecycleConnectFailureData):
|
231
243
|
""" Log message when connection failed
|
232
244
|
"""
|
233
|
-
|
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}")
|
234
266
|
return
|
235
267
|
|
236
268
|
def on_lifecycle_stopped(self, lifecycle_stopped_data: mqtt5.LifecycleStoppedData):
|
@@ -242,13 +274,22 @@ class EmeraldHWS():
|
|
242
274
|
def on_lifecycle_disconnection(self, lifecycle_disconnect_data: mqtt5.LifecycleDisconnectData):
|
243
275
|
""" Log message when disconnected
|
244
276
|
"""
|
245
|
-
|
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}")
|
246
285
|
return
|
247
286
|
|
248
287
|
def on_lifecycle_attempting_connect(self, lifecycle_attempting_connect_data: mqtt5.LifecycleAttemptingConnectData):
|
249
288
|
""" Log message when attempting connect
|
250
289
|
"""
|
251
|
-
|
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}")
|
252
293
|
return
|
253
294
|
|
254
295
|
def check_connection_health(self):
|
@@ -265,6 +306,14 @@ class EmeraldHWS():
|
|
265
306
|
if time_since_last_message > self.health_check_interval:
|
266
307
|
# This is an INFO level log because it's an important event
|
267
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
|
+
|
268
317
|
self.reconnectMQTT(reason="health_check")
|
269
318
|
else:
|
270
319
|
# This is a DEBUG level log to avoid cluttering logs
|
@@ -288,7 +337,7 @@ class EmeraldHWS():
|
|
288
337
|
for heat_pump in heat_pumps:
|
289
338
|
if heat_pump['id'] == id:
|
290
339
|
heat_pump['last_state'][key] = value
|
291
|
-
if self.update_callback
|
340
|
+
if self.update_callback is not None:
|
292
341
|
self.update_callback()
|
293
342
|
|
294
343
|
def subscribeForUpdates(self, id):
|
@@ -305,7 +354,8 @@ class EmeraldHWS():
|
|
305
354
|
topic_filter=mqtt_topic,
|
306
355
|
qos=mqtt5.QoS.AT_LEAST_ONCE)]))
|
307
356
|
|
308
|
-
|
357
|
+
# Wait for subscription to complete
|
358
|
+
subscribe_future.result(20)
|
309
359
|
|
310
360
|
def getFullStatus(self, id):
|
311
361
|
""" Returns a dict with the full status of the specified HWS
|
@@ -1,19 +1,20 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: emerald_hws
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.12
|
4
4
|
Summary: A package to manipulate and monitor Emerald Heat Pump Hot Water Systems
|
5
5
|
Author-email: Ross Williamson <ross@inertia.net.nz>
|
6
|
+
License-Expression: MIT
|
6
7
|
Project-URL: Homepage, https://github.com/ross-w/emerald_hws_py
|
7
8
|
Project-URL: Bug Tracker, https://github.com/ross-w/emerald_hws_py/issues
|
8
9
|
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
10
|
Classifier: Operating System :: OS Independent
|
11
11
|
Requires-Python: >=3.7
|
12
12
|
Description-Content-Type: text/markdown
|
13
|
-
|
14
|
-
Requires-Dist:
|
15
|
-
Requires-Dist:
|
16
|
-
|
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"
|
17
18
|
|
18
19
|
# emerald_hws_py
|
19
20
|
Python package for controlling Emerald Heat Pump Hot Water Systems
|
@@ -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
|
@@ -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.11"
|
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
|
-
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|