forked from UBayouski/DysonPureLinkPlugin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dyson_pure_link_device.py
177 lines (134 loc) · 5.83 KB
/
dyson_pure_link_device.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Dyson Pure Link Device Logic"""
import base64, json, hashlib, os, time, yaml
import paho.mqtt.client as mqtt
from Queue import Queue, Empty
from value_types import CONNECTION_STATE, DISCONNECTION_STATE, FanMode, StandbyMonitoring, ConnectionError, DisconnectionError, SensorsData, StateData
class DysonPureLink(object):
"""Plugin to connect to Dyson Pure Link device and get its sensors readings"""
def __init__(self):
self.client = None
self.config = None
self.connected = Queue()
self.disconnected = Queue()
self.state_data_available = Queue()
self.sensor_data_available = Queue()
self.sensor_data = None
self.state_data = None
self._is_connected = None
@property
def has_valid_data(self):
return self.sensor_data and self.sensor_data.has_data
@property
def password(self):
return self.config['DYSON_PASSWORD']
@property
def serial_number(self):
return self.config['DYSON_SERIAL']
@property
def device_type(self):
return self.config['DYSON_TYPE']
@property
def ip_address(self):
return self.config['DYSON_IP']
@property
def port_number(self):
return self.config['DYSON_PORT']
@property
def device_command(self):
return '{0}/{1}/command'.format(self.device_type, self.serial_number)
@property
def device_status(self):
return '{0}/{1}/status/current'.format(self.device_type, self.serial_number)
@staticmethod
def on_connect(client, userdata, flags, return_code):
"""Static callback to handle on_connect event"""
# Connection is successful with return_code: 0
if return_code:
userdata.connected.put_nowait(False)
raise ConnectionError(return_code)
# We subscribe to the status message
client.subscribe(userdata.device_status)
userdata.connected.put_nowait(True)
@staticmethod
def on_disconnect(client, userdata, return_code):
"""Static callback to handle on_disconnect event"""
if return_code:
raise DisconnectionError(return_code)
userdata.disconnected.put_nowait(True)
@staticmethod
def on_message(client, userdata, message):
"""Static callback to handle incoming messages"""
payload = message.payload.decode("utf-8")
json_message = json.loads(payload)
if StateData.is_state_data(json_message):
userdata.state_data_available.put_nowait(StateData(json_message))
if SensorsData.is_sensors_data(json_message):
userdata.sensor_data_available.put_nowait(SensorsData(json_message))
def _request_state(self):
"""Publishes request for current state message"""
if self.client:
command = json.dumps({
'msg': 'REQUEST-CURRENT-STATE',
'time': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())})
self.client.publish(self.device_command, command);
def _change_state(self, data):
"""Publishes request for change state message"""
if self.client:
command = json.dumps({
'msg': 'STATE-SET',
'time': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'mode-reason': 'LAPP',
'data': data
})
self.client.publish(self.device_command, command, 1)
self.state_data = self.state_data_available.get(timeout=5)
def _hashed_password(self):
"""Hash password (found in manual) to a base64 encoded of its shad512 value"""
hash = hashlib.sha512()
hash.update(self.password.encode('utf-8'))
return base64.b64encode(hash.digest()).decode('utf-8')
def parse_config(self):
"""Parses config file if any"""
file_name = '{}/dyson_pure_link.yaml'.format(os.path.dirname(os.path.abspath(__file__)))
if os.path.isfile(file_name):
self.config = yaml.safe_load(open(file_name))
return self.config
def connect_device(self):
"""
Connects to device using provided connection arguments
Returns: True/False depending on the result of connection
"""
self.client = mqtt.Client(clean_session=True, protocol=mqtt.MQTTv311, userdata=self)
self.client.username_pw_set(self.serial_number, self._hashed_password())
self.client.on_connect = self.on_connect
self.client.on_disconnect = self.on_disconnect
self.client.on_message = self.on_message
self.client.connect(self.ip_address, port=self.port_number)
self.client.loop_start()
self._is_connected = self.connected.get(timeout=10)
if self._is_connected:
self._request_state()
self.state_data = self.state_data_available.get(timeout=5)
self.sensor_data = self.sensor_data_available.get(timeout=5)
# Return True in case of successful connect and data retrieval
return True
# If any issue occurred return False
self.client = None
return False
def set_fan_mode(self, mode):
"""Changes fan mode: ON|OFF|AUTO"""
if self._is_connected:
self._change_state({'fmod': mode})
def set_standby_monitoring(self, mode):
"""Changes standby monitoring: ON|OFF"""
if self._is_connected:
self._change_state({'rhtm': mode})
def get_data(self):
return (self.state_data, self.sensor_data) if self.has_valid_data else tuple()
def disconnect_device(self):
"""Disconnects device and return the boolean result"""
if self.client:
self.client.loop_stop()
self.client.disconnect()
# Wait until we get on disconnect message
return self.disconnected.get(timeout=5)