emerald-hws 0.0.6__py3-none-any.whl → 0.0.8__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
@@ -4,7 +4,9 @@ import os
4
4
  import logging
5
5
  import boto3
6
6
  import random
7
- from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
7
+ import threading
8
+ from awsiot import mqtt5_client_builder, mqtt_connection_builder
9
+ from awscrt import mqtt5, http, auth, io
8
10
 
9
11
 
10
12
  class EmeraldHWS():
@@ -20,10 +22,11 @@ class EmeraldHWS():
20
22
  MQTT_HOST = "a13v32g67itvz9-ats.iot.ap-southeast-2.amazonaws.com"
21
23
  COGNITO_IDENTITY_POOL_ID = "ap-southeast-2:f5bbb02c-c00e-4f10-acb3-e7d1b05268e8"
22
24
 
23
- def __init__(self, email, password):
25
+ def __init__(self, email, password, update_callback=None):
24
26
  """ Initialise the API client
25
27
  :param email: The email address for logging into the Emerald app
26
28
  :param password: The password for the supplied user account
29
+ :param update_callback: Optional callback function to be called when an update is available
27
30
  """
28
31
 
29
32
  self.email = email
@@ -31,6 +34,7 @@ class EmeraldHWS():
31
34
  self.token = ""
32
35
  self.properties = {}
33
36
  self.logger = logging.getLogger()
37
+ self.update_callback = update_callback
34
38
 
35
39
  def getLoginToken(self):
36
40
  """ Performs an API request to get a token from the API
@@ -72,58 +76,56 @@ class EmeraldHWS():
72
76
  post_response_json = post_response.json()
73
77
 
74
78
  if post_response_json.get("code") == 200:
75
- self.logger.debug("Successfully logged into Emerald API")
79
+ self.logger.debug("emeraldhws: Successfully logged into Emerald API")
76
80
  self.properties = post_response_json.get("info").get("property")
77
81
  else:
78
82
  raise Exception("Unable to fetch properties from Emerald API")
79
83
 
80
- def getTemporaryCreds(self):
81
- """ Returns temporary credentials for IoT Core
84
+ def reconnectMQTT(self):
85
+ """ Stops an existing MQTT connection and creates a new one
82
86
  """
83
87
 
84
- identityPoolID = self.COGNITO_IDENTITY_POOL_ID
85
- region = self.MQTT_HOST.split('.')[2]
86
- cognitoIdentityClient = boto3.client('cognito-identity', region_name=region)
87
-
88
- temporaryIdentityId = cognitoIdentityClient.get_id(IdentityPoolId=identityPoolID)
89
- identityID = temporaryIdentityId["IdentityId"]
90
- self.logger.debug("AWS IoT IdentityID: {}".format(identityID))
91
-
92
- temporaryCredentials = cognitoIdentityClient.get_credentials_for_identity(IdentityId=identityID)
93
- self.logger.debug("Got new temporary credentials for AWS")
94
- self.identityID = identityID
95
- self.temporaryCredentials = temporaryCredentials
96
-
88
+ self.logger.debug("emeraldhws: awsiot: Tearing down and reconnecting to prevent stale connection")
89
+ self.mqttClient.stop()
90
+ self.connectMQTT()
91
+ self.subscribeAllHWS()
97
92
 
98
93
  def connectMQTT(self):
99
94
  """ Establishes a connection to Amazon IOT core's MQTT service
100
95
  """
101
96
 
102
97
  cert_path = os.path.join(os.path.dirname(__file__), '__assets__', 'SFSRootCAG2.pem')
103
- self.getTemporaryCreds()
104
-
105
- AccessKeyId = self.temporaryCredentials["Credentials"]["AccessKeyId"]
106
- SecretKey = self.temporaryCredentials["Credentials"]["SecretKey"]
107
- SessionToken = self.temporaryCredentials["Credentials"]["SessionToken"]
108
-
109
- # Init AWSIoTMQTTClient
110
- myAWSIoTMQTTClient = AWSIoTMQTTClient(self.identityID, useWebsocket=True)
111
-
112
- # AWSIoTMQTTClient configuration
113
- myAWSIoTMQTTClient.configureEndpoint(self.MQTT_HOST, 443)
114
- myAWSIoTMQTTClient.configureCredentials(cert_path)
115
- myAWSIoTMQTTClient.configureIAMCredentials(AccessKeyId, SecretKey, SessionToken)
116
- myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20)
117
- myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1) # Infinite offline Publish queueing
118
- myAWSIoTMQTTClient.configureDrainingFrequency(2) # Draining: 2 Hz
119
- myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10) # 10 sec
120
- myAWSIoTMQTTClient.configureMQTTOperationTimeout(10) # 10 sec
121
- myAWSIoTMQTTClient.onOffline = self.on_offline
122
- myAWSIoTMQTTClient.onOnline = self.on_online
123
- # Connect and subscribe to AWS IoT
124
- myAWSIoTMQTTClient.connect(keepAliveIntervalSecond=60)
125
-
126
- self.myAWSIoTMQTTClient = myAWSIoTMQTTClient
98
+ identityPoolID = self.COGNITO_IDENTITY_POOL_ID
99
+ region = self.MQTT_HOST.split('.')[2]
100
+ cognito_endpoint = "cognito-identity." + region + ".amazonaws.com"
101
+ cognitoIdentityClient = boto3.client('cognito-identity', region_name=region)
102
+
103
+ temporaryIdentityId = cognitoIdentityClient.get_id(IdentityPoolId=identityPoolID)
104
+ identityID = temporaryIdentityId["IdentityId"]
105
+ self.logger.debug("emeraldhws: awsiot: AWS IoT IdentityID: {}".format(identityID))
106
+
107
+ credentials_provider = auth.AwsCredentialsProvider.new_cognito(
108
+ endpoint=cognito_endpoint,
109
+ identity=identityID,
110
+ tls_ctx=io.ClientTlsContext(io.TlsContextOptions()))
111
+
112
+ client = mqtt5_client_builder.websockets_with_default_aws_signing(
113
+ endpoint = self.MQTT_HOST,
114
+ region = region,
115
+ credentials_provider = credentials_provider,
116
+ on_connection_interrupted = self.on_connection_interrupted,
117
+ on_connection_resumed = self.on_connection_resumed,
118
+ on_lifecycle_connection_success = self.on_lifecycle_connection_success,
119
+ on_lifecycle_stopped = self.on_lifecycle_stopped,
120
+ on_lifecycle_attempting_connect = self.on_lifecycle_attempting_connect,
121
+ on_lifecycle_disconnection = self.on_lifecycle_disconnection,
122
+ on_lifecycle_connection_failure = self.on_lifecycle_connection_failure,
123
+ on_publish_received = self.mqttCallback
124
+ )
125
+
126
+ client.start()
127
+ self.mqttClient = client
128
+ threading.Timer(43200.0, self.reconnectMQTT).start() # 12 hours
127
129
 
128
130
  def mqttDecodeUpdate(self, topic, payload):
129
131
  """ Attempt to decode a received MQTT message and direct appropriately
@@ -139,28 +141,53 @@ class EmeraldHWS():
139
141
  for key in json_payload[1]:
140
142
  self.updateHWSState(hws_id, key, json_payload[1][key])
141
143
 
142
- def mqttCallback(self, client, userdata, message):
144
+ def mqttCallback(self, publish_packet_data):
143
145
  """ Calls decode update for received message
144
146
  """
147
+ publish_packet = publish_packet_data.publish_packet
148
+ assert isinstance(publish_packet, mqtt5.PublishPacket)
149
+ self.logger.debug("emeraldhws: awsiot: Received message from MQTT topic {}: {}".format(publish_packet.topic, publish_packet.payload))
150
+ self.mqttDecodeUpdate(publish_packet.topic, publish_packet.payload)
145
151
 
146
- self.logger.debug("Received message from MQTT topic {}: {}".format(message.topic,message.payload.decode("utf-8")))
147
- self.mqttDecodeUpdate(message.topic, message.payload)
152
+ def on_connection_interrupted(self, connection, error, **kwargs):
153
+ """ Log error when MQTT is interrupted
154
+ """
155
+ self.logger.debug("emeraldhws: awsiot: Connection interrupted. error: {}".format(error))
148
156
 
149
- def on_offline(self):
150
- """ Reconfigures temporary credentials
157
+ def on_connection_resumed(self, connection, return_code, session_present, **kwargs):
158
+ """ Log message when MQTT is resumed
151
159
  """
160
+ self.logger.debug("emeraldhws: awsiot: Connection resumed. return_code: {} session_present: {}".format(return_code, session_present))
152
161
 
153
- self.logger.debug("AWS IoT offline")
154
- self.getTemporaryCreds()
155
- AccessKeyId = self.temporaryCredentials["Credentials"]["AccessKeyId"]
156
- SecretKey = self.temporaryCredentials["Credentials"]["SecretKey"]
157
- SessionToken = self.temporaryCredentials["Credentials"]["SessionToken"]
158
- self.myAWSIoTMQTTClient.configureIAMCredentials(AccessKeyId, SecretKey, SessionToken)
162
+ def on_lifecycle_connection_success(self, lifecycle_connect_success_data: mqtt5.LifecycleConnectSuccessData):
163
+ """ Log message when connection succeeded
164
+ """
165
+ self.logger.debug("emeraldhws: awsiot: connection succeeded")
166
+ return
167
+
168
+ def on_lifecycle_connection_failure(self, lifecycle_connection_failure: mqtt5.LifecycleConnectFailureData):
169
+ """ Log message when connection failed
170
+ """
171
+ self.logger.debug("emeraldhws: awsiot: connection failed")
172
+ return
159
173
 
160
- def on_online(self):
161
- """ Logs online state
174
+ def on_lifecycle_stopped(self, lifecycle_stopped_data: mqtt5.LifecycleStoppedData):
175
+ """ Log message when stopped
162
176
  """
163
- self.logger.debug("AWS IoT online")
177
+ self.logger.debug("emeraldhws: awsiot: stopped")
178
+ return
179
+
180
+ def on_lifecycle_disconnection(self, lifecycle_disconnect_data: mqtt5.LifecycleDisconnectData):
181
+ """ Log message when disconnected
182
+ """
183
+ self.logger.debug("emeraldhws: awsiot: disconnected")
184
+ return
185
+
186
+ def on_lifecycle_attempting_connect(self, lifecycle_attempting_connect_data: mqtt5.LifecycleAttemptingConnectData):
187
+ """ Log message when attempting connect
188
+ """
189
+ self.logger.debug("emeraldhws: awsiot: attempting to connect")
190
+ return
164
191
 
165
192
  def updateHWSState(self, id, key, value):
166
193
  """ Updates the specified value for the supplied key in the HWS id specified
@@ -174,17 +201,24 @@ class EmeraldHWS():
174
201
  for heat_pump in heat_pumps:
175
202
  if heat_pump['id'] == id:
176
203
  heat_pump['last_state'][key] = value
204
+ if self.update_callback != None:
205
+ self.update_callback()
177
206
 
178
207
  def subscribeForUpdates(self, id):
179
208
  """ Subscribes to the MQTT topics for the supplied HWS
180
209
  :param id: The UUID of the requested HWS
181
210
  """
182
- if not self.myAWSIoTMQTTClient:
211
+ if not self.mqttClient:
183
212
  self.connectMQTT()
184
213
 
185
- # self.myAWSIoTMQTTClient.subscribe("ep/heat_pump/to_gw/{}".format(id), 1, self.mqttCallback)
186
- self.myAWSIoTMQTTClient.subscribe("ep/heat_pump/from_gw/{}".format(id), 1, self.mqttCallback)
187
- # self.myAWSIoTMQTTClient.subscribe("ep/heat_pump/custom/topic/{}".format(id), 1, self.mqttCallback)
214
+ mqtt_topic = "ep/heat_pump/from_gw/{}".format(id)
215
+ subscribe_future = self.mqttClient.subscribe(
216
+ subscribe_packet=mqtt5.SubscribePacket(
217
+ subscriptions=[mqtt5.Subscription(
218
+ topic_filter=mqtt_topic,
219
+ qos=mqtt5.QoS.AT_LEAST_ONCE)]))
220
+
221
+ suback = subscribe_future.result(20)
188
222
 
189
223
  def getFullStatus(self, id):
190
224
  """ Returns a dict with the full status of the specified HWS
@@ -221,42 +255,47 @@ class EmeraldHWS():
221
255
  },
222
256
  payload
223
257
  ]
224
-
225
- self.myAWSIoTMQTTClient.publish("ep/heat_pump/to_gw/{}".format(id), json.dumps(msg), 1)
258
+ mqtt_topic = "ep/heat_pump/to_gw/{}".format(id)
259
+ publish_future = self.mqttClient.publish(
260
+ mqtt5.PublishPacket(
261
+ topic=mqtt_topic,
262
+ payload=json.dumps(msg),
263
+ qos=mqtt5.QoS.AT_LEAST_ONCE))
264
+ publish_future.result(20) # 20 seconds
226
265
 
227
266
  def turnOn(self, id):
228
267
  """ Turns the specified HWS on
229
268
  :param id: The UUID of the HWS to turn on
230
269
  """
231
- self.logger.debug("Sending control message: turn on")
270
+ self.logger.debug("emeraldhws: Sending control message: turn on")
232
271
  self.sendControlMessage(id, {"switch":1})
233
272
 
234
273
  def turnOff(self, id):
235
274
  """ Turns the specified HWS off
236
275
  :param id: The UUID of the HWS to turn off
237
276
  """
238
- self.logger.debug("Sending control message: turn off")
277
+ self.logger.debug("emeraldhws: Sending control message: turn off")
239
278
  self.sendControlMessage(id, {"switch":0})
240
279
 
241
280
  def setNormalMode(self, id):
242
281
  """ Sets the specified HWS to normal (not Boost or Quiet) mode
243
282
  :param id: The UUID of the HWS to set to normal mode
244
283
  """
245
- self.logger.debug("Sending control message: normal mode")
284
+ self.logger.debug("emeraldhws: Sending control message: normal mode")
246
285
  self.sendControlMessage(id, {"mode":1})
247
286
 
248
287
  def setBoostMode(self, id):
249
288
  """ Sets the specified HWS to boost (high power) mode
250
289
  :param id: The UUID of the HWS to set to boost mode
251
290
  """
252
- self.logger.debug("Sending control message: boost mode")
291
+ self.logger.debug("emeraldhws: Sending control message: boost mode")
253
292
  self.sendControlMessage(id, {"mode":0})
254
293
 
255
294
  def setQuietMode(self, id):
256
295
  """ Sets the specified HWS to quiet (low power) mode
257
296
  :param id: The UUID of the HWS to set to quiet mode
258
297
  """
259
- self.logger.debug("Sending control message: quiet mode")
298
+ self.logger.debug("emeraldhws: Sending control message: quiet mode")
260
299
  self.sendControlMessage(id, {"mode":2})
261
300
 
262
301
  def isOn(self, id):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emerald_hws
3
- Version: 0.0.6
3
+ Version: 0.0.8
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
  Project-URL: Homepage, https://github.com/ross-w/emerald_hws_py
@@ -12,7 +12,7 @@ Requires-Python: >=3.7
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: boto3
15
- Requires-Dist: AWSIoTPythonSDK
15
+ Requires-Dist: awsiotsdk
16
16
 
17
17
  # emerald_hws_py
18
18
  Python package for controlling Emerald Heat Pump Hot Water Systems
@@ -0,0 +1,8 @@
1
+ emerald_hws/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
+ emerald_hws/emeraldhws.py,sha256=tsTBTs0ZrBgAkvERT6FqYI8iNqFEkg4LvLijnsWp3eY,13892
3
+ emerald_hws/__assets__/SFSRootCAG2.pem,sha256=hw9W0AnYrrlbcWsOewAgIl1ULEsoO57Ylu35dCjWcS4,1424
4
+ emerald_hws-0.0.8.dist-info/LICENSE,sha256=zzMi56JX7OO-epbXNfe_oFW6sfZHSlO7Yxm0Oh0V014,1072
5
+ emerald_hws-0.0.8.dist-info/METADATA,sha256=4AvoaGEg0ITTsgGTW24_mbDu83ECe-ovrE0c5VQgKms,688
6
+ emerald_hws-0.0.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
7
+ emerald_hws-0.0.8.dist-info/top_level.txt,sha256=ZCiUmnBkDr2n4QVkTet1s_AKiGJjuz3heuCR5w5ZqLY,12
8
+ emerald_hws-0.0.8.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- emerald_hws/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
- emerald_hws/emeraldhws.py,sha256=w7iSSo9hcsk8CoQsYD89Ar-PWyzVkjnpbxd8YEXrujc,12051
3
- emerald_hws/__assets__/SFSRootCAG2.pem,sha256=hw9W0AnYrrlbcWsOewAgIl1ULEsoO57Ylu35dCjWcS4,1424
4
- emerald_hws-0.0.6.dist-info/LICENSE,sha256=zzMi56JX7OO-epbXNfe_oFW6sfZHSlO7Yxm0Oh0V014,1072
5
- emerald_hws-0.0.6.dist-info/METADATA,sha256=jHPA5zbpaLofBPi47Pkx0U7Jrpf9xTurvIAMuC-bh10,694
6
- emerald_hws-0.0.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
7
- emerald_hws-0.0.6.dist-info/top_level.txt,sha256=ZCiUmnBkDr2n4QVkTet1s_AKiGJjuz3heuCR5w5ZqLY,12
8
- emerald_hws-0.0.6.dist-info/RECORD,,