emerald-hws 0.0.11__py3-none-any.whl → 0.0.12__py3-none-any.whl
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/__init__.py +2 -0
- emerald_hws/emeraldhws.py +62 -12
- {emerald_hws-0.0.11.dist-info → emerald_hws-0.0.12.dist-info}/METADATA +7 -6
- emerald_hws-0.0.12.dist-info/RECORD +7 -0
- emerald_hws-0.0.11.dist-info/RECORD +0 -8
- emerald_hws-0.0.11.dist-info/licenses/LICENSE +0 -21
- {emerald_hws-0.0.11.dist-info → emerald_hws-0.0.12.dist-info}/WHEEL +0 -0
- {emerald_hws-0.0.11.dist-info → emerald_hws-0.0.12.dist-info}/top_level.txt +0 -0
emerald_hws/__init__.py
CHANGED
emerald_hws/emeraldhws.py
CHANGED
@@ -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
|
@@ -0,0 +1,7 @@
|
|
1
|
+
emerald_hws/__init__.py,sha256=uukjQ-kiPYKWvGT3jLL6kJA1DCNAxtw4HlLKqPSypXs,61
|
2
|
+
emerald_hws/emeraldhws.py,sha256=wfsZXOR_MlL3FEPCrTz3eU2fhY1xFCW9ztODI50hL04,22092
|
3
|
+
emerald_hws/__assets__/SFSRootCAG2.pem,sha256=hw9W0AnYrrlbcWsOewAgIl1ULEsoO57Ylu35dCjWcS4,1424
|
4
|
+
emerald_hws-0.0.12.dist-info/METADATA,sha256=cKfX2Ye1-KtmUzZFm8o91gazAZ9NNO3HIO61QD9VJv0,2534
|
5
|
+
emerald_hws-0.0.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
+
emerald_hws-0.0.12.dist-info/top_level.txt,sha256=ZCiUmnBkDr2n4QVkTet1s_AKiGJjuz3heuCR5w5ZqLY,12
|
7
|
+
emerald_hws-0.0.12.dist-info/RECORD,,
|
@@ -1,8 +0,0 @@
|
|
1
|
-
emerald_hws/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
2
|
-
emerald_hws/emeraldhws.py,sha256=h8I-bJUq7RFlveWyzNsahjdt5h7zWPdrbmCX3Q99atg,19019
|
3
|
-
emerald_hws/__assets__/SFSRootCAG2.pem,sha256=hw9W0AnYrrlbcWsOewAgIl1ULEsoO57Ylu35dCjWcS4,1424
|
4
|
-
emerald_hws-0.0.11.dist-info/licenses/LICENSE,sha256=zzMi56JX7OO-epbXNfe_oFW6sfZHSlO7Yxm0Oh0V014,1072
|
5
|
-
emerald_hws-0.0.11.dist-info/METADATA,sha256=mezYMqZpFZ1RnAkWj17mQHl6c4_wqRTduE1aU5_SRZk,2472
|
6
|
-
emerald_hws-0.0.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
7
|
-
emerald_hws-0.0.11.dist-info/top_level.txt,sha256=ZCiUmnBkDr2n4QVkTet1s_AKiGJjuz3heuCR5w5ZqLY,12
|
8
|
-
emerald_hws-0.0.11.dist-info/RECORD,,
|
@@ -1,21 +0,0 @@
|
|
1
|
-
MIT License
|
2
|
-
|
3
|
-
Copyright (c) 2024 Ross Williamson
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
7
|
-
in the Software without restriction, including without limitation the rights
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
10
|
-
furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
13
|
-
copies or substantial portions of the Software.
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
SOFTWARE.
|
File without changes
|
File without changes
|