emerald-hws 0.0.11__py3-none-any.whl → 0.0.13__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 CHANGED
@@ -1 +1,3 @@
1
+ from .emeraldhws import EmeraldHWS
1
2
 
3
+ __all__ = ["EmeraldHWS"]
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 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():
@@ -36,7 +37,7 @@ class EmeraldHWS():
36
37
  self.password = password
37
38
  self.token = ""
38
39
  self.properties = {}
39
- self.logger = logging.getLogger()
40
+ self.logger = logging.getLogger("emerald_hws")
40
41
  self.update_callback = update_callback
41
42
 
42
43
  # Convert minutes to seconds for internal use
@@ -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.13
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,7 @@
1
+ emerald_hws/__init__.py,sha256=uukjQ-kiPYKWvGT3jLL6kJA1DCNAxtw4HlLKqPSypXs,61
2
+ emerald_hws/emeraldhws.py,sha256=ZkUbQcGYlj3gkpVjAcPfX6XLsxrmEX-m0PqyIXmdmII,22105
3
+ emerald_hws/__assets__/SFSRootCAG2.pem,sha256=hw9W0AnYrrlbcWsOewAgIl1ULEsoO57Ylu35dCjWcS4,1424
4
+ emerald_hws-0.0.13.dist-info/METADATA,sha256=PikQFknGIWDbzc3-tUKY6ZpFHcrNXnkLcZjui_aDjPg,2534
5
+ emerald_hws-0.0.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ emerald_hws-0.0.13.dist-info/top_level.txt,sha256=ZCiUmnBkDr2n4QVkTet1s_AKiGJjuz3heuCR5w5ZqLY,12
7
+ emerald_hws-0.0.13.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.