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 +106 -67
- {emerald_hws-0.0.6.dist-info → emerald_hws-0.0.8.dist-info}/METADATA +2 -2
- emerald_hws-0.0.8.dist-info/RECORD +8 -0
- emerald_hws-0.0.6.dist-info/RECORD +0 -8
- {emerald_hws-0.0.6.dist-info → emerald_hws-0.0.8.dist-info}/LICENSE +0 -0
- {emerald_hws-0.0.6.dist-info → emerald_hws-0.0.8.dist-info}/WHEEL +0 -0
- {emerald_hws-0.0.6.dist-info → emerald_hws-0.0.8.dist-info}/top_level.txt +0 -0
emerald_hws/emeraldhws.py
CHANGED
@@ -4,7 +4,9 @@ import os
|
|
4
4
|
import logging
|
5
5
|
import boto3
|
6
6
|
import random
|
7
|
-
|
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
|
81
|
-
"""
|
84
|
+
def reconnectMQTT(self):
|
85
|
+
""" Stops an existing MQTT connection and creates a new one
|
82
86
|
"""
|
83
87
|
|
84
|
-
|
85
|
-
|
86
|
-
|
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.
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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,
|
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
|
-
|
147
|
-
|
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
|
150
|
-
"""
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
161
|
-
"""
|
174
|
+
def on_lifecycle_stopped(self, lifecycle_stopped_data: mqtt5.LifecycleStoppedData):
|
175
|
+
""" Log message when stopped
|
162
176
|
"""
|
163
|
-
self.logger.debug("
|
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.
|
211
|
+
if not self.mqttClient:
|
183
212
|
self.connectMQTT()
|
184
213
|
|
185
|
-
|
186
|
-
self.
|
187
|
-
|
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.
|
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.
|
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:
|
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,,
|
File without changes
|
File without changes
|
File without changes
|