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.
@@ -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.11
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
- License-File: LICENSE
14
- Requires-Dist: boto3
15
- Requires-Dist: awsiotsdk
16
- Dynamic: license-file
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
@@ -0,0 +1,3 @@
1
+ from .emeraldhws import EmeraldHWS
2
+
3
+ __all__ = ["EmeraldHWS"]
@@ -1,13 +1,14 @@
1
1
  import json
2
- import requests
3
- import os
4
2
  import logging
5
- import boto3
3
+ import os
6
4
  import random
7
5
  import threading
8
6
  import time
9
- from awsiot import mqtt5_client_builder, mqtt_connection_builder
10
- from awscrt import mqtt5, http, auth, io
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
- cert_path = os.path.join(os.path.dirname(__file__), '__assets__', 'SFSRootCAG2.pem')
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
- self.logger.debug("emeraldhws: awsiot: Connection interrupted. error: {}".format(error))
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
- self.logger.debug("emeraldhws: awsiot: connection failed")
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
- self.logger.debug("emeraldhws: awsiot: disconnected")
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
- self.logger.debug("emeraldhws: awsiot: attempting to connect")
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 != None:
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
- suback = subscribe_future.result(20)
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.11
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
- License-File: LICENSE
14
- Requires-Dist: boto3
15
- Requires-Dist: awsiotsdk
16
- Dynamic: license-file
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
@@ -0,0 +1,6 @@
1
+ boto3<2.0.0,>=1.40.0
2
+ awsiotsdk<2.0.0,>=1.24.0
3
+ requests>=2.25.0
4
+
5
+ [dev]
6
+ ruff<1.0.0,>=0.12.0
@@ -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,2 +0,0 @@
1
- boto3
2
- awsiotsdk
File without changes
File without changes
File without changes