emerald-hws 0.0.10__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 +148 -17
- emerald_hws-0.0.12.dist-info/METADATA +80 -0
- emerald_hws-0.0.12.dist-info/RECORD +7 -0
- {emerald_hws-0.0.10.dist-info → emerald_hws-0.0.12.dist-info}/WHEEL +1 -1
- emerald_hws-0.0.10.dist-info/LICENSE +0 -21
- emerald_hws-0.0.10.dist-info/METADATA +0 -18
- emerald_hws-0.0.10.dist-info/RECORD +0 -8
- {emerald_hws-0.0.10.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,12 +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
|
-
|
9
|
-
|
6
|
+
import time
|
7
|
+
|
8
|
+
import boto3
|
9
|
+
import requests
|
10
|
+
from awscrt import mqtt5, auth, io
|
11
|
+
from awsiot import mqtt5_client_builder
|
10
12
|
|
11
13
|
|
12
14
|
class EmeraldHWS():
|
@@ -22,11 +24,13 @@ class EmeraldHWS():
|
|
22
24
|
MQTT_HOST = "a13v32g67itvz9-ats.iot.ap-southeast-2.amazonaws.com"
|
23
25
|
COGNITO_IDENTITY_POOL_ID = "ap-southeast-2:f5bbb02c-c00e-4f10-acb3-e7d1b05268e8"
|
24
26
|
|
25
|
-
def __init__(self, email, password, update_callback=None):
|
27
|
+
def __init__(self, email, password, update_callback=None, connection_timeout_minutes=720, health_check_minutes=60):
|
26
28
|
""" Initialise the API client
|
27
29
|
:param email: The email address for logging into the Emerald app
|
28
30
|
:param password: The password for the supplied user account
|
29
31
|
:param update_callback: Optional callback function to be called when an update is available
|
32
|
+
:param connection_timeout_minutes: Optional timeout in minutes before reconnecting MQTT (default: 720 minutes/12 hours)
|
33
|
+
:param health_check_minutes: Optional interval in minutes to check for message activity (default: 60 minutes/1 hour)
|
30
34
|
"""
|
31
35
|
|
32
36
|
self.email = email
|
@@ -35,6 +39,27 @@ class EmeraldHWS():
|
|
35
39
|
self.properties = {}
|
36
40
|
self.logger = logging.getLogger()
|
37
41
|
self.update_callback = update_callback
|
42
|
+
|
43
|
+
# Convert minutes to seconds for internal use
|
44
|
+
self.connection_timeout = connection_timeout_minutes * 60.0
|
45
|
+
self.health_check_interval = health_check_minutes * 60.0 if health_check_minutes > 0 else 0
|
46
|
+
self.last_message_time = None
|
47
|
+
self.health_check_timer = None
|
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
|
+
|
54
|
+
# Ensure reasonable minimum values (e.g., at least 5 minutes for connection timeout)
|
55
|
+
if connection_timeout_minutes < 5 and connection_timeout_minutes != 0:
|
56
|
+
self.logger.warning("emeraldhws: Connection timeout too short, setting to minimum of 5 minutes")
|
57
|
+
self.connection_timeout = 5 * 60.0
|
58
|
+
|
59
|
+
# Ensure reasonable minimum values for health check (e.g., at least 5 minutes)
|
60
|
+
if 0 < health_check_minutes < 5:
|
61
|
+
self.logger.warning("emeraldhws: Health check interval too short, setting to minimum of 5 minutes")
|
62
|
+
self.health_check_interval = 5 * 60.0
|
38
63
|
|
39
64
|
def getLoginToken(self):
|
40
65
|
""" Performs an API request to get a token from the API
|
@@ -87,20 +112,48 @@ class EmeraldHWS():
|
|
87
112
|
|
88
113
|
self.update_callback = update_callback
|
89
114
|
|
90
|
-
def reconnectMQTT(self):
|
115
|
+
def reconnectMQTT(self, reason="scheduled"):
|
91
116
|
""" Stops an existing MQTT connection and creates a new one
|
117
|
+
:param reason: Reason for reconnection (scheduled, health_check, etc.)
|
92
118
|
"""
|
93
|
-
|
94
|
-
|
119
|
+
self.logger.info(f"emeraldhws: awsiot: Reconnecting MQTT connection (reason: {reason})")
|
120
|
+
|
121
|
+
# Store current temperature values for comparison after reconnect
|
122
|
+
temp_values = {}
|
123
|
+
for properties in self.properties:
|
124
|
+
heat_pumps = properties.get('heat_pump', [])
|
125
|
+
for heat_pump in heat_pumps:
|
126
|
+
hws_id = heat_pump['id']
|
127
|
+
if 'last_state' in heat_pump and 'temp_current' in heat_pump['last_state']:
|
128
|
+
temp_values[hws_id] = heat_pump['last_state']['temp_current']
|
129
|
+
|
95
130
|
self.mqttClient.stop()
|
96
131
|
self.connectMQTT()
|
97
132
|
self.subscribeAllHWS()
|
133
|
+
|
134
|
+
# After reconnection, check if temperatures have changed
|
135
|
+
def check_temp_changes():
|
136
|
+
for properties in self.properties:
|
137
|
+
heat_pumps = properties.get('heat_pump', [])
|
138
|
+
for heat_pump in heat_pumps:
|
139
|
+
hws_id = heat_pump['id']
|
140
|
+
if (hws_id in temp_values and
|
141
|
+
'last_state' in heat_pump and
|
142
|
+
'temp_current' in heat_pump['last_state']):
|
143
|
+
old_temp = temp_values[hws_id]
|
144
|
+
new_temp = heat_pump['last_state']['temp_current']
|
145
|
+
if old_temp != new_temp:
|
146
|
+
self.logger.info(f"emeraldhws: Temperature changed after reconnect for {hws_id}: {old_temp} → {new_temp}")
|
147
|
+
|
148
|
+
# Check for temperature changes after a short delay to allow for updates
|
149
|
+
threading.Timer(10.0, check_temp_changes).start()
|
98
150
|
|
99
151
|
def connectMQTT(self):
|
100
152
|
""" Establishes a connection to Amazon IOT core's MQTT service
|
101
153
|
"""
|
102
154
|
|
103
|
-
|
155
|
+
# Certificate path is available but not currently used in the connection
|
156
|
+
# os.path.join(os.path.dirname(__file__), '__assets__', 'SFSRootCAG2.pem')
|
104
157
|
identityPoolID = self.COGNITO_IDENTITY_POOL_ID
|
105
158
|
region = self.MQTT_HOST.split('.')[2]
|
106
159
|
cognito_endpoint = "cognito-identity." + region + ".amazonaws.com"
|
@@ -131,7 +184,16 @@ class EmeraldHWS():
|
|
131
184
|
|
132
185
|
client.start()
|
133
186
|
self.mqttClient = client
|
134
|
-
|
187
|
+
|
188
|
+
# Schedule periodic reconnection using configurable timeout
|
189
|
+
if self.connection_timeout > 0:
|
190
|
+
threading.Timer(self.connection_timeout, self.reconnectMQTT).start()
|
191
|
+
|
192
|
+
# Start health check timer if enabled
|
193
|
+
if self.health_check_interval > 0:
|
194
|
+
self.health_check_timer = threading.Timer(self.health_check_interval, self.check_connection_health)
|
195
|
+
self.health_check_timer.daemon = True
|
196
|
+
self.health_check_timer.start()
|
135
197
|
|
136
198
|
def mqttDecodeUpdate(self, topic, payload):
|
137
199
|
""" Attempt to decode a received MQTT message and direct appropriately
|
@@ -153,12 +215,15 @@ class EmeraldHWS():
|
|
153
215
|
publish_packet = publish_packet_data.publish_packet
|
154
216
|
assert isinstance(publish_packet, mqtt5.PublishPacket)
|
155
217
|
self.logger.debug("emeraldhws: awsiot: Received message from MQTT topic {}: {}".format(publish_packet.topic, publish_packet.payload))
|
218
|
+
self.last_message_time = time.time() # Update the last message time
|
156
219
|
self.mqttDecodeUpdate(publish_packet.topic, publish_packet.payload)
|
157
220
|
|
158
221
|
def on_connection_interrupted(self, connection, error, **kwargs):
|
159
222
|
""" Log error when MQTT is interrupted
|
160
223
|
"""
|
161
|
-
|
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}")
|
162
227
|
|
163
228
|
def on_connection_resumed(self, connection, return_code, session_present, **kwargs):
|
164
229
|
""" Log message when MQTT is resumed
|
@@ -169,12 +234,35 @@ class EmeraldHWS():
|
|
169
234
|
""" Log message when connection succeeded
|
170
235
|
"""
|
171
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"
|
172
240
|
return
|
173
241
|
|
174
242
|
def on_lifecycle_connection_failure(self, lifecycle_connection_failure: mqtt5.LifecycleConnectFailureData):
|
175
243
|
""" Log message when connection failed
|
176
244
|
"""
|
177
|
-
|
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}")
|
178
266
|
return
|
179
267
|
|
180
268
|
def on_lifecycle_stopped(self, lifecycle_stopped_data: mqtt5.LifecycleStoppedData):
|
@@ -186,14 +274,56 @@ class EmeraldHWS():
|
|
186
274
|
def on_lifecycle_disconnection(self, lifecycle_disconnect_data: mqtt5.LifecycleDisconnectData):
|
187
275
|
""" Log message when disconnected
|
188
276
|
"""
|
189
|
-
|
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}")
|
190
285
|
return
|
191
286
|
|
192
287
|
def on_lifecycle_attempting_connect(self, lifecycle_attempting_connect_data: mqtt5.LifecycleAttemptingConnectData):
|
193
288
|
""" Log message when attempting connect
|
194
289
|
"""
|
195
|
-
|
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}")
|
196
293
|
return
|
294
|
+
|
295
|
+
def check_connection_health(self):
|
296
|
+
""" Check if we've received any messages recently, reconnect if not
|
297
|
+
"""
|
298
|
+
if self.last_message_time is None:
|
299
|
+
# No messages received yet, don't reconnect
|
300
|
+
self.logger.debug("emeraldhws: awsiot: Health check - No messages received yet")
|
301
|
+
else:
|
302
|
+
current_time = time.time()
|
303
|
+
time_since_last_message = current_time - self.last_message_time
|
304
|
+
minutes_since_last = time_since_last_message / 60.0
|
305
|
+
|
306
|
+
if time_since_last_message > self.health_check_interval:
|
307
|
+
# This is an INFO level log because it's an important event
|
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
|
+
|
317
|
+
self.reconnectMQTT(reason="health_check")
|
318
|
+
else:
|
319
|
+
# This is a DEBUG level log to avoid cluttering logs
|
320
|
+
self.logger.debug(f"emeraldhws: awsiot: Health check - Last message received {minutes_since_last:.1f} minutes ago")
|
321
|
+
|
322
|
+
# Schedule next health check
|
323
|
+
if self.health_check_interval > 0:
|
324
|
+
self.health_check_timer = threading.Timer(self.health_check_interval, self.check_connection_health)
|
325
|
+
self.health_check_timer.daemon = True
|
326
|
+
self.health_check_timer.start()
|
197
327
|
|
198
328
|
def updateHWSState(self, id, key, value):
|
199
329
|
""" Updates the specified value for the supplied key in the HWS id specified
|
@@ -207,7 +337,7 @@ class EmeraldHWS():
|
|
207
337
|
for heat_pump in heat_pumps:
|
208
338
|
if heat_pump['id'] == id:
|
209
339
|
heat_pump['last_state'][key] = value
|
210
|
-
if self.update_callback
|
340
|
+
if self.update_callback is not None:
|
211
341
|
self.update_callback()
|
212
342
|
|
213
343
|
def subscribeForUpdates(self, id):
|
@@ -224,7 +354,8 @@ class EmeraldHWS():
|
|
224
354
|
topic_filter=mqtt_topic,
|
225
355
|
qos=mqtt5.QoS.AT_LEAST_ONCE)]))
|
226
356
|
|
227
|
-
|
357
|
+
# Wait for subscription to complete
|
358
|
+
subscribe_future.result(20)
|
228
359
|
|
229
360
|
def getFullStatus(self, id):
|
230
361
|
""" Returns a dict with the full status of the specified HWS
|
@@ -0,0 +1,80 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: emerald_hws
|
3
|
+
Version: 0.0.12
|
4
|
+
Summary: A package to manipulate and monitor Emerald Heat Pump Hot Water Systems
|
5
|
+
Author-email: Ross Williamson <ross@inertia.net.nz>
|
6
|
+
License-Expression: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/ross-w/emerald_hws_py
|
8
|
+
Project-URL: Bug Tracker, https://github.com/ross-w/emerald_hws_py/issues
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.7
|
12
|
+
Description-Content-Type: text/markdown
|
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"
|
18
|
+
|
19
|
+
# emerald_hws_py
|
20
|
+
Python package for controlling Emerald Heat Pump Hot Water Systems
|
21
|
+
|
22
|
+
## Overview
|
23
|
+
This package provides an interface to control and monitor Emerald Heat Pump Hot Water Systems through their API and MQTT service.
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
```bash
|
27
|
+
pip install emerald_hws
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
```python
|
32
|
+
from emerald_hws.emeraldhws import EmeraldHWS
|
33
|
+
|
34
|
+
# Basic usage with default connection settings
|
35
|
+
client = EmeraldHWS("your_email@example.com", "your_password")
|
36
|
+
client.connect()
|
37
|
+
|
38
|
+
# List all hot water systems
|
39
|
+
hws_list = client.listHWS()
|
40
|
+
print(f"Found {len(hws_list)} hot water systems")
|
41
|
+
|
42
|
+
# Get status of first HWS
|
43
|
+
hws_id = hws_list[0]
|
44
|
+
status = client.getFullStatus(hws_id)
|
45
|
+
print(f"Current temperature: {status['last_state'].get('temp_current')}")
|
46
|
+
|
47
|
+
# Turn on the hot water system
|
48
|
+
client.turnOn(hws_id)
|
49
|
+
```
|
50
|
+
|
51
|
+
## Configuration Options
|
52
|
+
|
53
|
+
### Connection Timeout
|
54
|
+
The module will automatically reconnect to the MQTT service periodically to prevent stale connections. You can configure this timeout:
|
55
|
+
|
56
|
+
```python
|
57
|
+
# Set connection timeout to 6 hours (360 minutes)
|
58
|
+
client = EmeraldHWS("your_email@example.com", "your_password", connection_timeout_minutes=360)
|
59
|
+
```
|
60
|
+
|
61
|
+
### Health Check
|
62
|
+
The module can proactively check for message activity and reconnect if no messages have been received for a specified period:
|
63
|
+
|
64
|
+
```python
|
65
|
+
# Set health check to check every 30 minutes
|
66
|
+
client = EmeraldHWS("your_email@example.com", "your_password", health_check_minutes=30)
|
67
|
+
|
68
|
+
# Disable health check
|
69
|
+
client = EmeraldHWS("your_email@example.com", "your_password", health_check_minutes=0)
|
70
|
+
```
|
71
|
+
|
72
|
+
## Callback for Updates
|
73
|
+
You can register a callback function to be notified when the state of any hot water system changes:
|
74
|
+
|
75
|
+
```python
|
76
|
+
def my_callback():
|
77
|
+
print("Hot water system state updated!")
|
78
|
+
|
79
|
+
client = EmeraldHWS("your_email@example.com", "your_password", update_callback=my_callback)
|
80
|
+
```
|
@@ -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,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.
|
@@ -1,18 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.2
|
2
|
-
Name: emerald_hws
|
3
|
-
Version: 0.0.10
|
4
|
-
Summary: A package to manipulate and monitor Emerald Heat Pump Hot Water Systems
|
5
|
-
Author-email: Ross Williamson <ross@inertia.net.nz>
|
6
|
-
Project-URL: Homepage, https://github.com/ross-w/emerald_hws_py
|
7
|
-
Project-URL: Bug Tracker, https://github.com/ross-w/emerald_hws_py/issues
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
|
-
Classifier: Operating System :: OS Independent
|
11
|
-
Requires-Python: >=3.7
|
12
|
-
Description-Content-Type: text/markdown
|
13
|
-
License-File: LICENSE
|
14
|
-
Requires-Dist: boto3
|
15
|
-
Requires-Dist: awsiotsdk
|
16
|
-
|
17
|
-
# emerald_hws_py
|
18
|
-
Python package for controlling Emerald Heat Pump Hot Water Systems
|
@@ -1,8 +0,0 @@
|
|
1
|
-
emerald_hws/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
2
|
-
emerald_hws/emeraldhws.py,sha256=aAH2x5f9c5sw2NBg0BSbPJ0iuzNdLaXWvWMK7h2hYeY,14361
|
3
|
-
emerald_hws/__assets__/SFSRootCAG2.pem,sha256=hw9W0AnYrrlbcWsOewAgIl1ULEsoO57Ylu35dCjWcS4,1424
|
4
|
-
emerald_hws-0.0.10.dist-info/LICENSE,sha256=zzMi56JX7OO-epbXNfe_oFW6sfZHSlO7Yxm0Oh0V014,1072
|
5
|
-
emerald_hws-0.0.10.dist-info/METADATA,sha256=8TYWMzh7CgGm7WBoOAdUwNyKzv0k2--LGRti97PM7DU,689
|
6
|
-
emerald_hws-0.0.10.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
7
|
-
emerald_hws-0.0.10.dist-info/top_level.txt,sha256=ZCiUmnBkDr2n4QVkTet1s_AKiGJjuz3heuCR5w5ZqLY,12
|
8
|
-
emerald_hws-0.0.10.dist-info/RECORD,,
|
File without changes
|