emerald-hws 0.0.15__py3-none-any.whl → 0.0.17__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/emeraldhws.py CHANGED
@@ -40,13 +40,15 @@ class EmeraldHWS():
40
40
  self.logger = logging.getLogger(__name__)
41
41
  self.update_callback = update_callback
42
42
  self._state_lock = threading.RLock() # Thread-safe lock for state operations
43
-
43
+ self._connection_event = threading.Event() # Event to signal when MQTT connection is established
44
+ self.mqttClient = None # Initialize to None
45
+
44
46
  # Convert minutes to seconds for internal use
45
47
  self.connection_timeout = connection_timeout_minutes * 60.0
46
48
  self.health_check_interval = health_check_minutes * 60.0 if health_check_minutes > 0 else 0
47
49
  self.last_message_time = None
48
50
  self.health_check_timer = None
49
-
51
+
50
52
  # Connection state tracking
51
53
  self.connection_state = "initial" # possible states: initial, connected, failed
52
54
  self.consecutive_failures = 0
@@ -118,7 +120,7 @@ class EmeraldHWS():
118
120
  :param reason: Reason for reconnection (scheduled, health_check, etc.)
119
121
  """
120
122
  self.logger.info(f"emeraldhws: awsiot: Reconnecting MQTT connection (reason: {reason})")
121
-
123
+
122
124
  # Store current temperature values for comparison after reconnect
123
125
  temp_values = {}
124
126
  for properties in self.properties:
@@ -127,8 +129,11 @@ class EmeraldHWS():
127
129
  hws_id = heat_pump['id']
128
130
  if 'last_state' in heat_pump and 'temp_current' in heat_pump['last_state']:
129
131
  temp_values[hws_id] = heat_pump['last_state']['temp_current']
130
-
131
- self.mqttClient.stop()
132
+
133
+ if self.mqttClient is not None:
134
+ self.mqttClient.stop()
135
+ self.mqttClient = None # Clear the client so a new one can be created
136
+
132
137
  self.connectMQTT()
133
138
  self.subscribeAllHWS()
134
139
 
@@ -153,6 +158,14 @@ class EmeraldHWS():
153
158
  """ Establishes a connection to Amazon IOT core's MQTT service
154
159
  """
155
160
 
161
+ # If already connected, skip
162
+ if self.mqttClient is not None:
163
+ self.logger.debug("emeraldhws: awsiot: MQTT client already exists, skipping connection")
164
+ return
165
+
166
+ # Clear the connection event before starting new connection
167
+ self._connection_event.clear()
168
+
156
169
  # Certificate path is available but not currently used in the connection
157
170
  # os.path.join(os.path.dirname(__file__), '__assets__', 'SFSRootCAG2.pem')
158
171
  identityPoolID = self.COGNITO_IDENTITY_POOL_ID
@@ -185,11 +198,16 @@ class EmeraldHWS():
185
198
 
186
199
  client.start()
187
200
  self.mqttClient = client
188
-
201
+
202
+ # Block until connection is established or timeout (30 seconds)
203
+ if not self._connection_event.wait(timeout=30):
204
+ self.logger.warning("emeraldhws: awsiot: Connection establishment timed out after 30 seconds")
205
+ # Continue anyway - the connection may still succeed asynchronously
206
+
189
207
  # Schedule periodic reconnection using configurable timeout
190
208
  if self.connection_timeout > 0:
191
209
  threading.Timer(self.connection_timeout, self.reconnectMQTT).start()
192
-
210
+
193
211
  # Start health check timer if enabled
194
212
  if self.health_check_interval > 0:
195
213
  self.health_check_timer = threading.Timer(self.health_check_interval, self.check_connection_health)
@@ -238,6 +256,8 @@ class EmeraldHWS():
238
256
  # Reset failure counter and update connection state
239
257
  self.consecutive_failures = 0
240
258
  self.connection_state = "connected"
259
+ # Signal that connection is established
260
+ self._connection_event.set()
241
261
  return
242
262
 
243
263
  def on_lifecycle_connection_failure(self, lifecycle_connection_failure: mqtt5.LifecycleConnectFailureData):
@@ -247,14 +267,18 @@ class EmeraldHWS():
247
267
  error_code = getattr(error, 'code', 'unknown')
248
268
  error_name = getattr(error, 'name', 'unknown')
249
269
  error_message = str(error)
250
-
270
+
251
271
  # Update connection state and increment failure counter
252
272
  self.connection_state = "failed"
253
273
  self.consecutive_failures += 1
254
-
274
+
255
275
  # Log at INFO level since this is important for troubleshooting
256
276
  self.logger.info(f"emeraldhws: awsiot: connection failed - Error: {error_name} (code: {error_code}), Message: {error_message}")
257
-
277
+
278
+ # Log additional error details if available
279
+ if hasattr(error, '__dict__'):
280
+ self.logger.debug(f"emeraldhws: awsiot: error details: {error.__dict__}")
281
+
258
282
  # If there's a CONNACK packet available, log its details too
259
283
  if hasattr(lifecycle_connection_failure, 'connack_packet') and lifecycle_connection_failure.connack_packet:
260
284
  connack = lifecycle_connection_failure.connack_packet
@@ -264,6 +288,17 @@ class EmeraldHWS():
264
288
  self.logger.info(f"emeraldhws: awsiot: MQTT CONNACK reason: {reason_code} - {reason_string}")
265
289
  else:
266
290
  self.logger.info(f"emeraldhws: awsiot: MQTT CONNACK reason code: {reason_code}")
291
+
292
+ # Log all CONNACK properties if available
293
+ if hasattr(connack, '__dict__'):
294
+ self.logger.debug(f"emeraldhws: awsiot: CONNACK details: {connack.__dict__}")
295
+ else:
296
+ self.logger.debug("emeraldhws: awsiot: no CONNACK packet available in failure data")
297
+
298
+ # Log the exception data structure itself for deeper debugging
299
+ if hasattr(lifecycle_connection_failure, '__dict__'):
300
+ self.logger.debug(f"emeraldhws: awsiot: failure data: {lifecycle_connection_failure.__dict__}")
301
+
267
302
  return
268
303
 
269
304
  def on_lifecycle_stopped(self, lifecycle_stopped_data: mqtt5.LifecycleStoppedData):
@@ -278,19 +313,26 @@ class EmeraldHWS():
278
313
  # Extract disconnect reason if available
279
314
  reason = "unknown reason"
280
315
  if hasattr(lifecycle_disconnect_data, 'disconnect_packet') and lifecycle_disconnect_data.disconnect_packet:
281
- reason_code = getattr(lifecycle_disconnect_data.disconnect_packet, 'reason_code', 'unknown')
282
- reason_string = getattr(lifecycle_disconnect_data.disconnect_packet, 'reason_string', '')
316
+ disconnect_packet = lifecycle_disconnect_data.disconnect_packet
317
+ reason_code = getattr(disconnect_packet, 'reason_code', 'unknown')
318
+ reason_string = getattr(disconnect_packet, 'reason_string', '')
283
319
  reason = f"reason code: {reason_code}" + (f" - {reason_string}" if reason_string else "")
284
-
320
+
321
+ # Log full disconnect packet details at debug level
322
+ if hasattr(disconnect_packet, '__dict__'):
323
+ self.logger.debug(f"emeraldhws: awsiot: disconnect packet details: {disconnect_packet.__dict__}")
324
+ else:
325
+ # Log the disconnect data structure if no packet available
326
+ if hasattr(lifecycle_disconnect_data, '__dict__'):
327
+ self.logger.debug(f"emeraldhws: awsiot: disconnect data: {lifecycle_disconnect_data.__dict__}")
328
+
285
329
  self.logger.info(f"emeraldhws: awsiot: disconnected - {reason}")
286
330
  return
287
331
 
288
332
  def on_lifecycle_attempting_connect(self, lifecycle_attempting_connect_data: mqtt5.LifecycleAttemptingConnectData):
289
333
  """ Log message when attempting connect
290
334
  """
291
- # Include endpoint information if available
292
- endpoint = getattr(lifecycle_attempting_connect_data, 'endpoint', 'unknown')
293
- self.logger.debug(f"emeraldhws: awsiot: attempting to connect to {endpoint}")
335
+ self.logger.debug("emeraldhws: awsiot: attempting to connect")
294
336
  return
295
337
 
296
338
  def check_connection_health(self):
@@ -459,10 +501,40 @@ class EmeraldHWS():
459
501
  """
460
502
  full_status = self.getFullStatus(id)
461
503
  if full_status:
504
+ # Try to get work_state from last_state (updated via MQTT)
505
+ if full_status.get("last_state") and "work_state" in full_status.get("last_state"):
506
+ work_state = full_status.get("last_state").get("work_state")
507
+ # work_state: 0=off/idle, 1=actively heating, 2=on but not heating
508
+ return (work_state == 1)
509
+
510
+ # Fallback to device_operation_status if work_state not available yet
511
+ # (e.g., before first MQTT update after initialization)
462
512
  heating_status = full_status.get("device_operation_status")
463
513
  return (heating_status == 1)
514
+
464
515
  return False
465
516
 
517
+ def getHourlyEnergyUsage(self, id):
518
+ """ Returns energy usage as reported by heater for the previous hour in kWh and a string of format YYYY-MM-DD HH:00 dictating the starting hour for the energy reading
519
+ :param id: The UUID of the HWS to query
520
+ """
521
+ full_status = self.getFullStatus(id)
522
+ if not full_status:
523
+ return None
524
+
525
+ consumption = full_status.get("consumption_data")
526
+ if consumption:
527
+ consumption = json.loads(consumption)
528
+ else:
529
+ return None
530
+
531
+ current_hour = consumption.get("current_hour")
532
+ last_data_at = consumption.get("last_data_at")
533
+ if current_hour is None or last_data_at is None:
534
+ return None
535
+
536
+ return current_hour, last_data_at
537
+
466
538
  def currentMode(self, id):
467
539
  """ Returns an integer specifying the current mode (0==boost, 1==normal, 2==quiet)
468
540
  :param id: The UUID of the HWS to query
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emerald_hws
3
- Version: 0.0.15
3
+ Version: 0.0.17
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
6
  License-Expression: MIT
@@ -0,0 +1,7 @@
1
+ emerald_hws/__init__.py,sha256=uukjQ-kiPYKWvGT3jLL6kJA1DCNAxtw4HlLKqPSypXs,61
2
+ emerald_hws/emeraldhws.py,sha256=ixNkwFXVEHLXtQhMmGi_ncfKhNDZWJdmlyuAW92tyyo,26201
3
+ emerald_hws/__assets__/SFSRootCAG2.pem,sha256=hw9W0AnYrrlbcWsOewAgIl1ULEsoO57Ylu35dCjWcS4,1424
4
+ emerald_hws-0.0.17.dist-info/METADATA,sha256=ytcqaOt_6fMzL1gGvoSybD3cQHiqLzPr5KTrPvO3rCc,2534
5
+ emerald_hws-0.0.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ emerald_hws-0.0.17.dist-info/top_level.txt,sha256=ZCiUmnBkDr2n4QVkTet1s_AKiGJjuz3heuCR5w5ZqLY,12
7
+ emerald_hws-0.0.17.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- emerald_hws/__init__.py,sha256=uukjQ-kiPYKWvGT3jLL6kJA1DCNAxtw4HlLKqPSypXs,61
2
- emerald_hws/emeraldhws.py,sha256=0yvazkMtwEfimqGsBe6Q3zGEfpQbCKgCiXDpRYwKFL4,22876
3
- emerald_hws/__assets__/SFSRootCAG2.pem,sha256=hw9W0AnYrrlbcWsOewAgIl1ULEsoO57Ylu35dCjWcS4,1424
4
- emerald_hws-0.0.15.dist-info/METADATA,sha256=6SQeBxgNLAu_nHDnT4VqJdFB_z7GtkKLixAlSbikCK0,2534
5
- emerald_hws-0.0.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- emerald_hws-0.0.15.dist-info/top_level.txt,sha256=ZCiUmnBkDr2n4QVkTet1s_AKiGJjuz3heuCR5w5ZqLY,12
7
- emerald_hws-0.0.15.dist-info/RECORD,,