-
Notifications
You must be signed in to change notification settings - Fork 2
/
plugin.py
827 lines (742 loc) · 45.4 KB
/
plugin.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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is furnished
# to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# Author: Jan-Jaap Kostelijk
#
# Domoticz plugin to handle communction to Dyson devices
#
"""
<plugin key="DysonPureLink" name="Dyson Pure Link" author="Jan-Jaap Kostelijk" version="5.0.2" wikilink="https://github.com/JanJaapKo/DysonPureLink/wiki" externallink="https://github.com/JanJaapKo/DysonPureLink">
<description>
<h2>Dyson Pure Link plugin</h2><br/>
Connects to Dyson Pure Link devices.
It reads the machine's states and sensors and it can control it via commands.<br/><br/>
This plugin has been tested with a PureCool type 475 (pre 2018), it is assumed the other types work too. There are known issues in retreiving information from the cloud account, see git page for the issues.<br/><br/>
<h2>Configuration</h2>
Configuration of the plugin is a 2 step action due to the 2 factor authentication. First, provide all in step A and when you receive an email, proceed wuith step B. See the Wiki for more info.<br/><br/>
<ol type="A">
<li>provide always the following information:</li>
<ol>
<li>the machine's IP adress</li>
<li>the port number (should normally remain 1883)</li>
<li>enter the email adress under "Cloud account email adress"</li>
<li>enter the password under "Cloud account password"</li>
<li>optional: enter the machine's name under "machine name" when there is more than 1 machines linked to the account</li>
</ol>
<li>When you have received a verification cpode via email, supply it once when recieved (can be removed after use):</li>
<ol>
<li>enter the received code under "email verification code"</li>
</ol>
</ol>
</description>
<params>
<param field="Address" label="IP Address" required="true"/>
<param field="Port" label="Port" width="30px" required="true" default="1883"/>
<param field="Mode5" label="Cloud account email adress" default="[email protected]" width="300px" required="false"/>
<param field="Mode3" label="Cloud account password" required="false" default="" password="true"/>
<param field="Mode1" label="email verification code" width="75px" default="0"/>
<param field="Mode6" label="Machine name (cloud account)" required="false" default=""/>
<param field="Mode4" label="Debug" width="75px">
<options>
<option label="Verbose" value="Verbose"/>
<option label="True" value="Debug"/>
<option label="False" value="Normal" default="true"/>
<option label="Reset cloud data" value="Reset"/>
</options>
</param>
<param field="Mode2" label="Refresh interval" width="75px">
<options>
<option label="20s" value="2"/>
<option label="1m" value="6"/>
<option label="5m" value="30" default="true"/>
<option label="10m" value="60"/>
<option label="15m" value="90"/>
</options>
</param>
</params>
</plugin>
"""
try:
import DomoticzEx as Domoticz
debug = False
except ImportError:
import fakeDomoticz as Domoticz
debug = True
import json
import time
from mqtt import MqttClient
from dyson_pure_link_device import DysonPureLinkDevice
from cloud.account import DysonAccount
from value_types import SensorsData, StateData
PING_COUNT = 6
class DysonPureLinkPlugin:
#define class variables
enabled = False
mqttClient = None
#unit numbers for devices to create
#for Pure Cool models
fanModeUnit = 1
nightModeUnit = 2
fanSpeedUnit = 3
fanOscillationUnit = 4
standbyMonitoringUnit = 5
filterLifeUnit = 6
qualityTargetUnit = 7
tempHumUnit = 8
volatileUnit = 9
particlesUnit = 10
sleepTimeUnit = 11
fanStateUnit = 12
fanFocusUnit = 13
fanModeAutoUnit = 14
particles2_5Unit = 15
particles10Unit = 16
nitrogenDioxideDensityUnit = 17
heatModeUnit = 18
heatTargetUnit = 19
heatStateUnit = 20
particlesMatter25Unit = 21
particlesMatter10Unit = 22
resetFilterLifeUnit = 23
deviceStatusUnit = 24
runCounter = 6
def __init__(self):
self.myDevice = None
self.password = None
self.ip_address = None
self.port_number = None
self.sensor_data = None
self.state_data = None
self.mqttClient = None
self.log_level = None
def onStart(self):
Domoticz.Debug("onStart called")
#read out parameters for local connection
self.ip_address = Parameters["Address"].strip()
self.port_number = Parameters["Port"].strip()
self.otp_code = Parameters['Mode1']
self.runCounter = int(Parameters['Mode2'])
self.log_level = Parameters['Mode4']
self.account_password = Parameters['Mode3']
self.account_email = Parameters['Mode5']
self.machine_name = Parameters['Mode6']
if self.log_level == 'Debug':
Domoticz.Debugging(2)
DumpConfigToLog()
if self.log_level == 'Verbose':
Domoticz.Debugging(1+2+4+8+16+64)
DumpConfigToLog()
if self.log_level == 'Reset':
Domoticz.Log("Plugin config will be erased to retreive new cloud account data")
Config = {}
Config = Domoticz.Configuration(Config)
Domoticz.Log("starting plugin version "+Parameters["Version"])
#PureLink needs polling, get from config
Domoticz.Heartbeat(10)
self.pingCounter = PING_COUNT # ping every minute
self.version = Parameters["Version"]
self.checkVersion(self.version)
mqtt_client_id = ""
#get list of devices from plugin configuration
deviceDict = self.get_device_names()
if deviceDict != None and len(deviceDict)>0:
Domoticz.Debug("Number of devices found in plugin configuration: '"+str(len(deviceDict))+"'")
else:
Domoticz.Log("No devices found in plugin configuration, request from Dyson cloud account")
#new authentication
Domoticz.Debug("=== start making connection to Dyson account, new method as of 2021 ===")
dysonAccount2 = DysonAccount()
challenge_id = getConfigItem(Key="challenge_id", Default = "")
setConfigItem(Key="challenge_id", Value = "") #clear after use
if challenge_id == "":
#request otp code via email when no code entered
challenge_id = dysonAccount2.login_email_otp(self.account_email, "NL")
setConfigItem(Key="challenge_id", Value = challenge_id)
Domoticz.Log('==== An OTP verification code had been requested, please check email and paste code into plugin=====')
return
else:
#verify the received code
if len(self.otp_code) < 6:
Domoticz.Error("invalid verification code supplied")
return
dysonAccount2.verify(self.otp_code, self.account_email, self.account_password, challenge_id)
setConfigItem(Key="challenge_id", Value = "") #reset challenge id as it is no longer valid
Parameters['Mode1'] = "0" #reset the stored otp code
#get list of devices info's
deviceDict = dysonAccount2.devices()
deviceNames = list(deviceDict.keys())
Domoticz.Log("Received new devices: " + str(deviceNames) + ", they will be stored in plugin configuration")
i=0
for device in deviceDict:
setConfigItem(Key="{0}.name".format(i), Value = deviceNames[i]) #store the name of the machine
Domoticz.Debug('Key="{0}.name", Value = {1}'.format(i, deviceNames[i])) #store the name of the machine
setConfigItem(Key="{0}.credential".format(deviceDict[deviceNames[i]].name), Value = deviceDict[deviceNames[i]].credential) #store the credential
Domoticz.Debug('Key="{0}.credential", Value = {1}'.format(deviceDict[deviceNames[i]].name, deviceDict[deviceNames[i]].credential)) #store the credential
setConfigItem(Key="{0}.serial".format(deviceDict[deviceNames[i]].name), Value = deviceDict[deviceNames[i]].serial) #store the serial
Domoticz.Debug('Key="{0}.serial", Value = {1}'.format(deviceDict[deviceNames[i]].name, deviceDict[deviceNames[i]].serial)) #store the serial
setConfigItem(Key="{0}.product_type".format(deviceDict[deviceNames[i]].name), Value = deviceDict[deviceNames[i]].product_type) #store the product_type
Domoticz.Debug('Key="{0}.product_type" , Value = {1}'.format(deviceDict[deviceNames[i]].name, deviceDict[deviceNames[i]].product_type)) #store the product_type
i = i + 1
if deviceDict == None or len(deviceDict)<1:
Domoticz.Error("No devices found in plugin configuration or Dyson cloud account")
return
else:
Domoticz.Debug("Number of devices in plugin: '"+str(len(deviceDict))+"'")
if deviceDict != None and len(deviceDict) > 0:
Domoticz.Debug("starting to create device with len(deviceDict) = {0} and self.machine_name = '{1}'".format(len(deviceDict),self.machine_name))
if len(self.machine_name) > 0:
if self.machine_name in deviceDict:
password, serialNumber, deviceType= self.get_device_config(self.machine_name)
Domoticz.Debug("password: {0}, serialNumber: {1}, deviceType: {2}".format(password, serialNumber, deviceType))
self.myDevice = DysonPureLinkDevice(password, serialNumber, deviceType, self.machine_name)
else:
Domoticz.Error("The configured device name '" + self.machine_name + "' was not found in the cloud account. Available options: " + str(list(deviceDict)))
return
elif len(deviceDict) == 1:
#myDeviceName = deviceDict[list(deviceDict)[0]]
myDeviceName = list(deviceDict.keys())
Domoticz.Debug("deviceDict: '" + str(deviceDict) + "', myDeviceName: " + str(myDeviceName) + " " + str(type(myDeviceName)))
Domoticz.Log("1 device found in plugin, none configured, assuming we need this one: '" + myDeviceName[0] + "'")
self.machine_name = myDeviceName[0]
password, serialNumber, deviceType= self.get_device_config(myDeviceName[0])
Domoticz.Debug("password: {0}, serialNumber: {1}, deviceType: {2}".format(password, serialNumber, deviceType))
self.myDevice = DysonPureLinkDevice(password, serialNumber, deviceType, self.machine_name)
else:
#more than 1 device returned in cloud and no name configured, which the the plugin can't handle
Domoticz.Error("More than 1 device found in cloud account but no device name given to select. Select and filter one from available options: " + str(list(deviceDict)))
return
#the Domoticz connection object takes username and pwd from the Parameters so write them back
Parameters['Username'] = self.myDevice.serial #take username from account
Parameters['Password'] = self.myDevice.password #override the default password with the one returned from the cloud
else:
Domoticz.Error("No usable credentials found")
return
#self.createDevices()
self.createDevicesEx(self.myDevice.name)
Domoticz.Log("Device instance created: " + str(self.myDevice))
self.base_topic = self.myDevice.device_base_topic
Domoticz.Debug("base topic defined: '"+self.base_topic+"'")
#create the connection
if self.myDevice != None:
self.mqttClient = MqttClient(self.ip_address, self.port_number, mqtt_client_id, self.onMQTTConnected, self.onMQTTDisconnected, self.onMQTTPublish, self.onMQTTSubscribed)
def onStop(self):
Domoticz.Debug("onStop called")
def onCommand(self, DeviceID, Unit, Command, Level, Hue):
Domoticz.Debug("DysonPureLink plugin: onCommand called for Device/Unit " + str(DeviceID) + "/" + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level))
topic = ''
payload = ''
arg = ''
fan_pwr_list = ['438','520','527', '438E' ]
if Unit == self.qualityTargetUnit and Level<=100:
topic, payload = self.myDevice.set_quality_target(Level)
if Unit == self.fanSpeedUnit and Level<=100:
arg="0000"+str(Level//10)
topic, payload = self.myDevice.set_fan_speed(arg[-4:]) #use last 4 characters as speed level or AUTO
self.mqttClient.Publish(topic, payload)
if Level>0:
#when setting a speed value, make sure that the fan is actually on
if self.myDevice.product_type in fan_pwr_list:
topic, payload = self.myDevice.set_fan_power("ON")
else:
topic, payload = self.myDevice.set_fan_mode("FAN")
else:
if self.myDevice.product_type in fan_pwr_list:
topic, payload = self.myDevice.set_fan_power("OFF")
else:
topic, payload = self.myDevice.set_fan_mode("OFF") #use last 4 characters as speed level or AUTO
if Unit == self.fanModeUnit or (Unit == self.fanSpeedUnit and Level>100):
if self.myDevice.product_type in fan_pwr_list:
if Level >= 30:
arg="ON"
#Switch to Auto
topic, payload = self.myDevice.set_fan_power(arg)
self.mqttClient.Publish(topic, payload)
topic, payload = self.myDevice.set_fan_mode_auto(arg)
elif Level == 20:
arg="ON"
#Switch on, auto depends on previous setting
topic, payload = self.myDevice.set_fan_power(arg)
else:
#Switch Off
arg='OFF'
topic, payload = self.myDevice.set_fan_power(arg)
else:
if Level == 10: arg="OFF"
if Level == 20: arg="FAN"
if Level >=30: arg="AUTO"
topic, payload = self.myDevice.set_fan_mode(arg)
if Unit == self.fanStateUnit:
Domoticz.Log("Unit Fans State is read only, no command sent")
if Unit == self.fanOscillationUnit:
topic, payload = self.myDevice.set_oscilation(str(Command).upper())
if Unit == self.fanFocusUnit:
topic, payload = self.myDevice.set_focus(str(Command).upper())
if Unit == self.fanModeAutoUnit:
topic, payload = self.myDevice.set_fan_mode_auto(str(Command).upper())
if Unit == self.standbyMonitoringUnit:
topic, payload = self.myDevice.set_standby_monitoring(str(Command).upper())
if Unit == self.nightModeUnit:
topic, payload = self.myDevice.set_night_mode(str(Command).upper())
if Unit == self.heatModeUnit:
if Level == 10: arg="OFF"
if Level == 20: arg="HEAT"
topic, payload = self.myDevice.set_heat_mode(arg)
if Unit == self.heatTargetUnit:
topic, payload = self.myDevice.set_heat_target(Level)
if Unit == self.resetFilterLifeUnit:
UpdateDeviceEx(self.myDevice.name, self.resetFilterLifeUnit,1,"On") #acknowlegde switching on
topic, payload = self.myDevice.reset_filter()
self.mqttClient.Publish(topic, payload)
def onConnect(self, Connection, Status, Description):
Domoticz.Debug("onConnect called: Connection '"+str(Connection)+"', Status: '"+str(Status)+"', Description: '"+Description+"'")
self.mqttClient.onConnect(Connection, Status, Description)
def onDisconnect(self, Connection):
self.mqttClient.onDisconnect(Connection)
def onMessage(self, Connection, Data):
self.mqttClient.onMessage(Connection, Data)
def onNotification(self, Name, Subject, Text, Status, Priority, Sound, ImageFile):
Domoticz.Log("DysonPureLink plugin: onNotification: " + Name + "," + Subject + "," + Text + "," + Status + "," + str(Priority) + "," + Sound + "," + ImageFile)
def onHeartbeat(self):
if self.myDevice != None:
self.runCounter = self.runCounter - 1
if self.runCounter <= 0:
Domoticz.Debug("DysonPureLink plugin: Poll unit")
self.runCounter = int(Parameters['Mode2'])
topic, payload = self.myDevice.request_state()
self.mqttClient.Publish(topic, payload) #ask for update of current status
Domoticz.Debug("Polling unit in " + str(self.runCounter) + " heartbeats.")
self.pingCounter = self.pingCounter - 1
if self.pingCounter <= 0:
Domoticz.Debug("DysonPureLink plugin: Ping unit")
self.pingCounter = PING_COUNT
self.mqttClient.onHeartbeat()
def onDeviceRemoved(self, DeviceID, unit):
Domoticz.Log("DysonPureLink plugin: onDeviceRemoved called for unit '" + str(unit) + "' on device '" + str(DeviceID)+"'")
def updateDevices(self):
"""Update the defined devices from incoming mesage info"""
#update the devices
if self.state_data.oscillation is not None:
UpdateDeviceEx(self.myDevice.name, self.fanOscillationUnit, self.state_data.oscillation.state, str(self.state_data.oscillation))
if self.state_data.night_mode is not None:
UpdateDeviceEx(self.myDevice.name, self.nightModeUnit, self.state_data.night_mode.state, str(self.state_data.night_mode))
# Fan speed
if self.state_data.fan_speed is not None:
f_rate = self.state_data.fan_speed
if (f_rate == "AUTO"):
nValueNew = 110
sValueNew = "110" # Auto
else:
nValueNew = (int(f_rate))*10
sValueNew = str((int(f_rate)) * 10)
if self.state_data.fan_mode is not None:
Domoticz.Debug("update fanspeed, state of FanMode: " + str(self.state_data.fan_mode))
if self.state_data.fan_mode.state == 0:
nValueNew = 0
sValueNew = "0"
UpdateDeviceEx(self.myDevice.name, self.fanSpeedUnit, nValueNew, sValueNew)
if self.state_data.fan_mode is not None:
UpdateDeviceEx(self.myDevice.name, self.fanModeUnit, self.state_data.fan_mode.state, str((self.state_data.fan_mode.state+1)*10))
if self.state_data.fan_state is not None:
UpdateDeviceEx(self.myDevice.name, self.fanStateUnit, self.state_data.fan_state.state, str((self.state_data.fan_state.state+1)*10))
if self.state_data.filter_life is not None:
UpdateDeviceEx(self.myDevice.name, self.filterLifeUnit, self.state_data.filter_life, str(self.state_data.filter_life))
if self.state_data.filter_life is not None:
nameBase, curValue = Devices[self.myDevice.name].Units[self.resetFilterLifeUnit].Name.split(":")
UpdateDeviceEx(self.myDevice.name, self.resetFilterLifeUnit, 0, "Off", Name = nameBase + ": " + str(self.state_data.filter_life) + " hrs")
if self.state_data.quality_target is not None:
UpdateDeviceEx(self.myDevice.name, self.qualityTargetUnit, self.state_data.quality_target.state, str((self.state_data.quality_target.state+1)*10))
if self.state_data.standby_monitoring is not None:
UpdateDeviceEx(self.myDevice.name, self.standbyMonitoringUnit, self.state_data.standby_monitoring.state, str((self.state_data.standby_monitoring.state+1)*10))
if self.state_data.fan_mode_auto is not None:
UpdateDeviceEx(self.myDevice.name, self.fanModeAutoUnit, self.state_data.fan_mode_auto.state, str((self.state_data.fan_mode_auto.state+1)*10))
if self.state_data.focus is not None:
UpdateDeviceEx(self.myDevice.name, self.fanFocusUnit, self.state_data.focus.state, str(self.state_data.focus))
if self.state_data.heat_mode is not None:
UpdateDeviceEx(self.myDevice.name, self.heatModeUnit, self.state_data.heat_mode.state, str((self.state_data.heat_mode.state+1)*10))
if self.state_data.heat_target is not None:
UpdateDeviceEx(self.myDevice.name, self.heatTargetUnit, 0, str(self.state_data.heat_target))
if self.state_data.heat_state is not None:
UpdateDeviceEx(self.myDevice.name, self.heatStateUnit, self.state_data.heat_state.state, str((self.state_data.heat_state.state+1)*10))
if self.state_data.error is not None and self.state_data.warning is not None:
status_string = "Error: {}<br> Warning: {}".format(self.state_data.error, self.state_data.warning)
UpdateDeviceEx(self.myDevice.name, self.deviceStatusUnit, 0, status_string)
Domoticz.Debug("update StateData: " + str(self.state_data))
def updateSensors(self):
"""Update the defined devices from incoming mesage info"""
#update the devices
if self.sensor_data.temperature is not None and self.sensor_data.humidity is not None :
tempNum = int(self.sensor_data.temperature)
humNum = int(self.sensor_data.humidity)
UpdateDeviceEx(self.myDevice.name, self.tempHumUnit, 1, str(self.sensor_data.temperature)[:4] +';'+ str(self.sensor_data.humidity) + ";1")
if self.sensor_data.volatile_compounds is not None:
UpdateDeviceEx(self.myDevice.name, self.volatileUnit, self.sensor_data.volatile_compounds, str(self.sensor_data.volatile_compounds))
if self.sensor_data.particles is not None:
UpdateDeviceEx(self.myDevice.name, self.particlesUnit, self.sensor_data.particles, str(self.sensor_data.particles))
if self.sensor_data.particles2_5 is not None:
UpdateDeviceEx(self.myDevice.name, self.particles2_5Unit, self.sensor_data.particles2_5, str(self.sensor_data.particles2_5))
if self.sensor_data.particles10 is not None:
UpdateDeviceEx(self.myDevice.name, self.particles10Unit, self.sensor_data.particles10, str(self.sensor_data.particles10))
if self.sensor_data.particulate_matter_25 is not None:
UpdateDeviceEx(self.myDevice.name, self.particlesMatter25Unit, self.sensor_data.particulate_matter_25, str(self.sensor_data.particulate_matter_25))
if self.sensor_data.particulate_matter_10 is not None:
UpdateDeviceEx(self.myDevice.name, self.particlesMatter10Unit, self.sensor_data.particulate_matter_10, str(self.sensor_data.particulate_matter_10))
if self.sensor_data.nitrogenDioxideDensity is not None:
UpdateDeviceEx(self.myDevice.name, self.nitrogenDioxideDensityUnit, self.sensor_data.nitrogenDioxideDensity, str(self.sensor_data.nitrogenDioxideDensity))
if self.sensor_data.heat_target is not None:
UpdateDeviceEx(self.myDevice.name, self.heatTargetUnit, self.sensor_data.heat_target, str(self.sensor_data.heat_target))
UpdateDeviceEx(self.myDevice.name, self.sleepTimeUnit, self.sensor_data.sleep_timer, str(self.sensor_data.sleep_timer))
if self.sensor_data.has_data:
Domoticz.Debug("update SensorData: " + str(self.sensor_data))
else:
Domoticz.Debug("partial SensorData to update")
def createDevices(self):
#check, per device, if it is created. If not,create it
Options = {"LevelActions" : "|||",
"LevelNames" : "|OFF|ON|AUTO",
"LevelOffHidden" : "true",
"SelectorStyle" : "1"}
if self.fanModeUnit not in Devices:
Domoticz.Device(Name='Fan mode', Unit=self.fanModeUnit, TypeName="Selector Switch", Image=7, Options=Options).Create()
if self.fanStateUnit not in Devices:
Domoticz.Device(Name='Fan state', Unit=self.fanStateUnit, Type=244, Subtype=62, Image=7, Switchtype=0).Create()
if self.heatStateUnit not in Devices:
Domoticz.Device(Name='Heating state', Unit=self.heatStateUnit, Type=244, Subtype=62, Image=7, Switchtype=0).Create()
if self.nightModeUnit not in Devices:
Domoticz.Device(Name='Night mode', Unit=self.nightModeUnit, Type=244, Subtype=62, Switchtype=0, Image=9).Create()
Options = {"LevelActions" : "|||||||||||",
"LevelNames" : "OFF|1|2|3|4|5|6|7|8|9|10|AUTO",
"LevelOffHidden" : "false",
"SelectorStyle" : "1"}
if self.fanSpeedUnit not in Devices:
Domoticz.Device(Name='Fan speed', Unit=self.fanSpeedUnit, TypeName="Selector Switch", Image=7, Options=Options).Create()
if self.fanOscillationUnit not in Devices:
Domoticz.Device(Name='Oscilation mode', Unit=self.fanOscillationUnit, Type=244, Subtype=62, Image=7, Switchtype=0).Create()
if self.standbyMonitoringUnit not in Devices:
Domoticz.Device(Name='Standby monitor', Unit=self.standbyMonitoringUnit, Type=244, Subtype=62,Image=7, Switchtype=0).Create()
if self.filterLifeUnit not in Devices:
Options: {'Custom': '1;hrs'}
Domoticz.Device(Name='Remaining filter life', Unit=self.filterLifeUnit, TypeName="Custom", Options=Options).Create()
if self.resetFilterLifeUnit not in Devices:
Domoticz.Device(Name='Reset filter: 0 hrs', Unit=self.resetFilterLifeUnit, TypeName="Switch", Image=9).Create()
if self.tempHumUnit not in Devices:
Domoticz.Device(Name='Temperature and Humidity', Unit=self.tempHumUnit, TypeName="Temp+Hum").Create()
if self.volatileUnit not in Devices:
Domoticz.Device(Name='Volatile organic', Unit=self.volatileUnit, TypeName="Air Quality").Create()
if self.sleepTimeUnit not in Devices:
Domoticz.Device(Name='Sleep timer', Unit=self.sleepTimeUnit, TypeName="Custom").Create()
if self.particlesUnit not in Devices:
Domoticz.Device(Name='Dust', Unit=self.particlesUnit, TypeName="Air Quality").Create()
if self.qualityTargetUnit not in Devices:
Options = {"LevelActions" : "|||",
"LevelNames" : "|Normal|Sensitive (Medium)|Very Sensitive (High)|Off",
"LevelOffHidden" : "true",
"SelectorStyle" : "1"}
Domoticz.Device(Name='Air quality setpoint', Unit=self.qualityTargetUnit, TypeName="Selector Switch", Image=7, Options=Options).Create()
if self.particles2_5Unit not in Devices:
Domoticz.Device(Name='Dust (PM 2,5)', Unit=self.particles2_5Unit, TypeName="Air Quality").Create()
if self.particles10Unit not in Devices:
Domoticz.Device(Name='Dust (PM 10)', Unit=self.particles10Unit, TypeName="Air Quality").Create()
if self.particlesMatter25Unit not in Devices:
Domoticz.Device(Name='Particles (PM 25)', Unit=self.particlesMatter25Unit, TypeName="Air Quality").Create()
if self.particlesMatter10Unit not in Devices:
Domoticz.Device(Name='Particles (PM 10)', Unit=self.particlesMatter10Unit, TypeName="Air Quality").Create()
if self.fanModeAutoUnit not in Devices:
Domoticz.Device(Name='Fan mode auto', Unit=self.fanModeAutoUnit, Type=244, Subtype=62, Image=7, Switchtype=0).Create()
if self.fanFocusUnit not in Devices:
Domoticz.Device(Name='Fan focus mode', Unit=self.fanFocusUnit, Type=244, Subtype=62, Image=7, Switchtype=0).Create()
if self.nitrogenDioxideDensityUnit not in Devices:
Domoticz.Device(Name='Nitrogen Dioxide Density (NOx)', Unit=self.nitrogenDioxideDensityUnit, TypeName="Air Quality").Create()
if self.heatModeUnit not in Devices:
Options = {"LevelActions" : "||",
"LevelNames" : "|Off|Heating",
"LevelOffHidden" : "true",
"SelectorStyle" : "1"}
Domoticz.Device(Name='Heat mode', Unit=self.heatModeUnit, TypeName="Selector Switch", Image=7, Options=Options).Create()
if self.heatTargetUnit not in Devices:
Domoticz.Device(Name='Heat target', Unit=self.heatTargetUnit, Type=242, Subtype=1).Create()
if self.deviceStatusUnit not in Devices:
Domoticz.Device(Name='Machine status', Unit=self.deviceStatusUnit, TypeName="Text", Image=7).Create()
return True
def createDevicesEx(self, deviceId):
#check, per device, if it is created. If not,create it
Options = {"LevelActions" : "|||",
"LevelNames" : "|OFF|ON|AUTO",
"LevelOffHidden" : "true",
"SelectorStyle" : "1"}
if deviceId not in Devices or (self.fanModeUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Fan mode', Unit=self.fanModeUnit, TypeName="Selector Switch", Image=7, Options=Options, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.fanStateUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Fan state', Unit=self.fanStateUnit, Type=244, Subtype=62, Image=7, Switchtype=0, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.heatStateUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Heating state', Unit=self.heatStateUnit, Type=244, Subtype=62, Image=7, Switchtype=0, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.nightModeUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Night mode', Unit=self.nightModeUnit, Type=244, Subtype=62, Switchtype=0, Image=9, DeviceID=deviceId).Create()
Options = {"LevelActions" : "|||||||||||",
"LevelNames" : "OFF|1|2|3|4|5|6|7|8|9|10|AUTO",
"LevelOffHidden" : "false",
"SelectorStyle" : "1"}
if deviceId not in Devices or (self.fanSpeedUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Fan speed', Unit=self.fanSpeedUnit, TypeName="Selector Switch", Image=7, Options=Options, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.fanOscillationUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Oscilation mode', Unit=self.fanOscillationUnit, Type=244, Subtype=62, Image=7, Switchtype=0, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.standbyMonitoringUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Standby monitor', Unit=self.standbyMonitoringUnit, Type=244, Subtype=62,Image=7, Switchtype=0, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.filterLifeUnit not in Devices[deviceId].Units):
Options: {'Custom': '1;hrs'}
Domoticz.Unit(Name='Remaining filter life', Unit=self.filterLifeUnit, TypeName="Custom", Options=Options, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.resetFilterLifeUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Reset filter: 0 hrs', Unit=self.resetFilterLifeUnit, TypeName="Switch", Image=9, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.tempHumUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Temperature and Humidity', Unit=self.tempHumUnit, TypeName="Temp+Hum", DeviceID=deviceId).Create()
if deviceId not in Devices or (self.volatileUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Volatile organic', Unit=self.volatileUnit, TypeName="Air Quality", DeviceID=deviceId).Create()
if deviceId not in Devices or (self.sleepTimeUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Sleep timer', Unit=self.sleepTimeUnit, TypeName="Custom", DeviceID=deviceId).Create()
if deviceId not in Devices or (self.particlesUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Dust', Unit=self.particlesUnit, TypeName="Air Quality", DeviceID=deviceId).Create()
if deviceId not in Devices or (self.qualityTargetUnit not in Devices[deviceId].Units):
Options = {"LevelActions" : "|||",
"LevelNames" : "|Normal|Sensitive (Medium)|Very Sensitive (High)|Off",
"LevelOffHidden" : "true",
"SelectorStyle" : "1"}
Domoticz.Unit(Name='Air quality setpoint', Unit=self.qualityTargetUnit, TypeName="Selector Switch", Image=7, Options=Options, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.particles2_5Unit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Dust (PM 2,5)', Unit=self.particles2_5Unit, TypeName="Air Quality", DeviceID=deviceId).Create()
if deviceId not in Devices or (self.particles10Unit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Dust (PM 10)', Unit=self.particles10Unit, TypeName="Air Quality", DeviceID=deviceId).Create()
if deviceId not in Devices or (self.particlesMatter25Unit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Particles (PM 25)', Unit=self.particlesMatter25Unit, TypeName="Air Quality", DeviceID=deviceId).Create()
if deviceId not in Devices or (self.particlesMatter10Unit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Particles (PM 10)', Unit=self.particlesMatter10Unit, TypeName="Air Quality", DeviceID=deviceId).Create()
if deviceId not in Devices or (self.fanModeAutoUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Fan mode auto', Unit=self.fanModeAutoUnit, Type=244, Subtype=62, Image=7, Switchtype=0, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.fanFocusUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Fan focus mode', Unit=self.fanFocusUnit, Type=244, Subtype=62, Image=7, Switchtype=0, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.nitrogenDioxideDensityUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Nitrogen Dioxide Density (NOx)', Unit=self.nitrogenDioxideDensityUnit, TypeName="Air Quality", DeviceID=deviceId).Create()
if deviceId not in Devices or (self.heatModeUnit not in Devices[deviceId].Units):
Options = {"LevelActions" : "||",
"LevelNames" : "|Off|Heating",
"LevelOffHidden" : "true",
"SelectorStyle" : "1"}
Domoticz.Unit(Name='Heat mode', Unit=self.heatModeUnit, TypeName="Selector Switch", Image=7, Options=Options, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.heatTargetUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Heat target', Unit=self.heatTargetUnit, Type=242, Subtype=1, DeviceID=deviceId).Create()
if deviceId not in Devices or (self.deviceStatusUnit not in Devices[deviceId].Units):
Domoticz.Unit(Name='Machine status', Unit=self.deviceStatusUnit, TypeName="Text", Image=7, DeviceID=deviceId).Create()
return True
def onMQTTConnected(self):
"""connection to device established"""
Domoticz.Debug("onMQTTConnected called")
Domoticz.Log("MQTT connection established")
self.mqttClient.Subscribe([self.base_topic + '/status/current', self.base_topic + '/status/connection', self.base_topic + '/status/faults']) #subscribe to all topics on the machine
topic, payload = self.myDevice.request_state()
self.mqttClient.Publish(topic, payload) #ask for update of current status
def onMQTTDisconnected(self):
Domoticz.Debug("onMQTTDisconnected")
def onMQTTSubscribed(self):
Domoticz.Debug("onMQTTSubscribed")
def onMQTTPublish(self, topic, message):
Domoticz.Debug("MQTT Publish: MQTT message incoming: " + topic + " " + str(message))
if (topic == self.base_topic + '/status/current'):
#update of the machine's status
if StateData.is_state_data(message):
Domoticz.Debug("machine state or state change recieved")
self.state_data = StateData(message)
self.updateDevices()
if SensorsData.is_sensors_data(message):
Domoticz.Debug("sensor state recieved")
self.sensor_data = SensorsData(message)
self.updateSensors()
if (topic == self.base_topic + '/status/connection'):
#connection status received
Domoticz.Debug("connection state recieved")
if (topic == self.base_topic + '/status/software'):
#connection status received
Domoticz.Debug("software state recieved")
if (topic == self.base_topic + '/status/summary'):
#connection status received
Domoticz.Debug("summary state recieved")
def checkVersion(self, version):
"""checks actual version against stored version as 'Ma.Mi.Pa' and checks if updates needed"""
#read version from stored configuration
ConfVersion = getConfigItem("plugin version", "0.0.0")
Domoticz.Log("Starting version: " + version )
MaCurrent,MiCurrent,PaCurrent = version.split('.')
MaConf,MiConf,PaConf = ConfVersion.split('.')
Domoticz.Debug("checking versions: current '{0}', config '{1}'".format(version, ConfVersion))
if int(MaConf) < int(MaCurrent):
Domoticz.Log("Major version upgrade: {0} -> {1}".format(MaConf,MaCurrent))
#add code to perform MAJOR upgrades
if int(MaConf) < 5:
can_continue = self.updateToEx()
elif int(MiConf) < int(MiCurrent):
Domoticz.Log("Minor version upgrade: {0} -> {1}".format(MiConf,MiCurrent))
#add code to perform MINOR upgrades
elif int(PaConf) < int(PaCurrent):
Domoticz.Log("Patch version upgrade: {0} -> {1}".format(PaConf,PaCurrent))
#add code to perform PATCH upgrades, if any
if ConfVersion != version:
#store new version info
self._setVersion(MaCurrent,MiCurrent,PaCurrent)
def updateToEx(self):
"""routine to check if we can update to the Domoticz extended plugin framework"""
if len(Devices)>0:
Domoticz.Error("Devices are present in legacy version. Extra devices will be created!")
Domoticz.Error("Check replacing devices in plugin wiki")
return True
else:
return True
def get_device_names(self):
"""find the amount of stored devices"""
Configurations = getConfigItem()
devices = {}
for x in Configurations:
if x.find(".") > -1 and x.split(".")[1] == "name":
devices[str(Configurations[x])] = x.split(".")[0]
Domoticz.Debug("get_device_names, list of configured devices: " + str(devices))
return devices
def get_device_config(self, name = ""):
"""fetch all relevant config items from Domoticz.Configuration for device with name"""
Configurations = getConfigItem()
for x in Configurations:
if x.split(".")[1] == "name":
Domoticz.Debug("Found a machine name: " + x + " value: '" + str(Configurations[x]) + "'")
if Configurations[x] == name:
password = getConfigItem(Key="{0}.{1}".format(name, "credential"))
serialNumber = getConfigItem(Key="{0}.{1}".format(name, "serial"))
deviceType = getConfigItem(Key="{0}.{1}".format(name, "product_type"))
return password, serialNumber, deviceType
return
def _setVersion(self, major, minor, patch):
#set configs
Domoticz.Debug("Setting version to {0}.{1}.{2}".format(major, minor, patch))
setConfigItem(Key="MajorVersion", Value=major)
setConfigItem(Key="MinorVersion", Value=minor)
setConfigItem(Key="patchVersion", Value=patch)
setConfigItem(Key="plugin version", Value="{0}.{1}.{2}".format(major, minor, patch))
def _storeCredentials(self, creds, auths):
#store credentials as config item
Domoticz.Debug("Storing credentials: " + str(creds) + " and auth object: " + str(auths))
currentCreds = getConfigItem(Key = "credentials", Default = None)
if currentCreds is None or currentCreds != creds:
Domoticz.Log("Credentials from user authentication do not match those stored in config, updating config")
setConfigItem(Key = "credentials", Value = creds)
return True
# Configuration Helpers
def getConfigItem(Key=None, Default={}):
Value = Default
try:
Config = Domoticz.Configuration()
if (Key != None):
Value = Config[Key] # only return requested key if there was one
else:
Value = Config # return the whole configuration if no key
except KeyError:
Value = Default
except Exception as inst:
Domoticz.Error("Domoticz.Configuration read failed: '"+str(inst)+"'")
return Value
def setConfigItem(Key=None, Value=None):
Config = {}
if type(Value) not in (str, int, float, bool, bytes, bytearray, list, dict):
Domoticz.Error("A value is specified of a not allowed type: '" + str(type(Value)) + "'")
return Config
try:
Config = Domoticz.Configuration()
if (Key != None):
Config[Key] = Value
else:
Config = Value # set whole configuration if no key specified
Config = Domoticz.Configuration(Config)
except Exception as inst:
Domoticz.Error("Domoticz.Configuration operation failed: '"+str(inst)+"'")
return Config
def UpdateDevice(Unit, nValue, sValue, BatteryLevel=255, AlwaysUpdate=False, Name=""):
if Unit not in Devices: return
newName=Name
if Devices[Unit].Name != Name and Name != "":
newName=Name
else:
newName = Devices[Unit].Name
if Devices[Unit].nValue != nValue\
or Devices[Unit].sValue != sValue\
or Devices[Unit].BatteryLevel != BatteryLevel\
or AlwaysUpdate == True\
or (newName != "" and newName != Devices[Unit].Name):
Devices[Unit].Update(nValue, str(sValue), BatteryLevel=BatteryLevel, Name=newName)
Domoticz.Debug("Update %s: nValue %s - sValue %s - BatteryLevel %s" % (
Devices[Unit].Name,
nValue,
sValue,
BatteryLevel
))
def UpdateDeviceEx(Device, Unit, nValue, sValue, AlwaysUpdate=False, Name=""):
# Make sure that the Domoticz device still exists (they can be deleted) before updating it
if (Device in Devices and Unit in Devices[Device].Units):
if (Devices[Device].Units[Unit].nValue != nValue) or (Devices[Device].Units[Unit].sValue != sValue) or AlwaysUpdate:
Domoticz.Log("Updating device '"+Devices[Device].Units[Unit].Name+ "' with current sValue '"+Devices[Device].Units[Unit].sValue+"' to '" +sValue+"'")
#try:
Devices[Device].Units[Unit].nValue = nValue
Devices[Device].Units[Unit].sValue = sValue
if Name != "":
Devices[Device].Units[Unit].Name = Name
Devices[Device].Units[Unit].Update()
#logging.debug("Update "+str(nValue)+":'"+str(sValue)+"' ("+Devices[Device].Units[Unit].Name+")")
# except:
# Domoticz.Error("Update of device failed: "+str(Unit)+"!")
# logging.error("Update of device failed: "+str(Unit)+"!")
else:
Domoticz.Error("trying to update a non-existent unit "+str(Unit)+" from device "+str(Device))
return
global _plugin
_plugin = DysonPureLinkPlugin()
def onStart():
global _plugin
_plugin.onStart()
def onStop():
global _plugin
_plugin.onStop()
def onConnect(Connection, Status, Description):
global _plugin
_plugin.onConnect(Connection, Status, Description)
def onDisconnect(Connection):
global _plugin
_plugin.onDisconnect(Connection)
def onMessage(Connection, Data):
global _plugin
_plugin.onMessage(Connection, Data)
def onCommand(DeviceID, Unit, Command, Level, Color):
global _plugin
_plugin.onCommand(DeviceID, Unit, Command, Level, Color)
def onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile):
global _plugin
_plugin.onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile)
def onHeartbeat():
global _plugin
_plugin.onHeartbeat()
def onDeviceRemoved(DeviceID, Unit):
global _plugin
_plugin.onDeviceRemoved(DeviceID, Unit)
# Generic helper functions
def DumpConfigToLog():
Domoticz.Debug("Parameter count: " + str(len(Parameters)))
for x in Parameters:
if Parameters[x] != "":
Domoticz.Debug( "Parameter '" + x + "':'" + str(Parameters[x]) + "'")
Configurations = getConfigItem()
Domoticz.Debug("Configuration count: " + str(len(Configurations)))
for x in Configurations:
if Configurations[x] != "":
Domoticz.Debug( "Configuration '" + x + "':'" + str(Configurations[x]) + "'")
Domoticz.Debug("Device count: " + str(len(Devices)))
for x in Devices:
Domoticz.Debug("Device: " + str(x) + " - " + str(Devices[x]))
return