-
Notifications
You must be signed in to change notification settings - Fork 4
/
I_Nest1.xml
1240 lines (1121 loc) · 49.8 KB
/
I_Nest1.xml
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
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0"?>
<!--
MiOS (Vera) Plugin for Nest Thermostats
Copyright (C) 2012 John W. Cocula and others
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<implementation>
<functions>
-- [The following is from Hugh Eaves' I_RTCOA_Wifi_ZoneThermostat1.xml.
-- He figured out how to work around problems in deploying compressed
-- modules. Please cite him as copyright owner under the GPL license terms.]
-- Using "require" to access compressed modules doesn't work if the
-- module is declared without using the "module" function.
-- (see http://bugs.micasaverde.com/view.php?id=2276 )
--
-- We work around this with a shell script that executes pluto-lzo
-- to decompress the module. The temp file is used to
-- avoid a race condition when multiple instances of this module
-- start at the same time. (to prevent one instance from loading a
-- partially decompressed file from another instance)
local decompressScript = [[
decompress_lzo_file() {
SRC_FILE=/etc/cmh-ludl/$1.lzo
DEST_FILE=/etc/cmh-ludl/$1
if [ ! -e $DEST_FILE -o $SRC_FILE -nt $DEST_FILE ]
then
TEMP_FILE=$(mktemp)
pluto-lzo d $SRC_FILE $TEMP_FILE
mv $TEMP_FILE $DEST_FILE
fi
}
]]
os.execute(decompressScript .. "decompress_lzo_file L_nest_dkjson.lua")
os.execute(decompressScript .. "decompress_lzo_file L_nest_urlcode.lua")
local MSG_CLASS = "Nest"
local DEBUG_MODE = true
local taskHandle = -1
local TASK_ERROR = 2
local TASK_ERROR_PERM = -2
local TASK_SUCCESS = 4
local TASK_BUSY = 1
-- utility functions
local function log(text, level)
luup.log(MSG_CLASS .. ": " .. text, (level or 1))
end
local function debug(text)
if (DEBUG_MODE) then
log("debug: " .. text, 35)
end
end
local function task(text, mode)
local mode = mode or TASK_ERROR
if (mode ~= TASK_SUCCESS) then
log("task: " .. text, 50)
end
taskHandle = luup.task(text, (mode == TASK_ERROR_PERM) and TASK_ERROR or mode, MSG_CLASS, taskHandle)
end
local function readVariableOrInit(lul_device, serviceId, name, defaultValue)
local var = luup.variable_get(serviceId, name, lul_device)
if (var == nil) then
var = defaultValue
luup.variable_set(serviceId, name, var, lul_device)
end
return var
end
local function writeVariable(lul_device, serviceId, name, value)
luup.variable_set(serviceId, name, value, lul_device)
end
local Logging = 0
local function writeVariableIfChanged(lul_device, serviceId, name, value)
local curValue = luup.variable_get(serviceId, name, lul_device)
if (Logging == 1) or (value ~= curValue) then
writeVariable(lul_device, serviceId, name, value)
end
return value ~= curValue
end
local function findChild(deviceId, label)
for k, v in pairs(luup.devices) do
if (v.device_num_parent == deviceId and v.id == label) then
return k
end
end
end
-- main functions
local PLUGIN_VERSION = "1.9"
local NEST_SID = "urn:watou-com:serviceId:Nest1"
local NEST_STRUCTURE_SID = "urn:watou-com:serviceId:NestStructure1"
local HOUSE_STATUS_SID = "urn:upnp-org:serviceId:HouseStatus1"
local TEMP_SENSOR_SID = "urn:upnp-org:serviceId:TemperatureSensor1"
local TEMP_SETPOINT_HEAT_SID = "urn:upnp-org:serviceId:TemperatureSetpoint1_Heat"
local TEMP_SETPOINT_COOL_SID = "urn:upnp-org:serviceId:TemperatureSetpoint1_Cool"
local HUMIDITY_SENSOR_SID = "urn:micasaverde-com:serviceId:HumiditySensor1"
local HVAC_FAN_SID = "urn:upnp-org:serviceId:HVAC_FanOperatingMode1"
local HVAC_USER_SID = "urn:upnp-org:serviceId:HVAC_UserOperatingMode1"
local HVAC_STATE_SID = "urn:micasaverde-com:serviceId:HVAC_OperatingState1"
local HA_DEVICE_SID = "urn:micasaverde-com:serviceId:HaDevice1"
local SWITCH_POWER_SID = "urn:upnp-org:serviceId:SwitchPower1"
local MCV_ENERGY_METERING_SID = "urn:micasaverde-com:serviceId:EnergyMetering1"
local SECURITY_SENSOR_SID = "urn:micasaverde-com:serviceId:SecuritySensor1"
local DEFAULT_POLL = 120
local MIN_POLL = 60
local SOON = "5" -- seconds
local NEST_UA = "Nest/1.1.0.10 CFNetwork/548.0.4"
local NEST_BATTERY_LOW = 3.6
local NEST_BATTERY_HIGH = 3.9
local PROTECT_BATTERY_LOW = 4200 -- guess, TBD
local PROTECT_BATTERY_HIGH = 5400 -- 1.8v full voltage of 3 lithium AA batteries
local STRUCTURE_ID_PREFIX = "loc."
local THERMOSTAT_ID_PREFIX = "therm."
local HUMIDISTAT_ID_PREFIX = "humid."
local SMOKE_ID_PREFIX = "smoke."
local CO_ID_PREFIX = "co."
local PARENT_DEVICE, https, urlcode
local dkjson = require("L_nest_dkjson")
local veraTemperatureScale = "F"
local function getVeraTemperatureScale()
local code, data = luup.inet.wget("http://localhost:3480/data_request?id=lu_sdata")
if (code == 0) then
data = json.decode(data)
end
veraTemperatureScale = ((code == 0) and (data ~= nil) and (data.temperature ~= nil)) and data.temperature or "F"
end
local function getStructureId(altid)
local i, j = string.find(altid, STRUCTURE_ID_PREFIX)
return (i == nil) and altid or string.sub(altid, j+1)
end
local function getThermostatId(altid)
local i, j = string.find(altid, THERMOSTAT_ID_PREFIX)
return (i == nil) and altid or string.sub(altid, j+1)
end
local function getHumidistatId(altid)
local i, j = string.find(altid, HUMIDISTAT_ID_PREFIX)
return (i == nil) and altid or string.sub(altid, j+1)
end
local function round(value, precision)
return (value >= 0) and
(math.floor(value * precision + 0.5) / precision) or
(math.ceil(value * precision - 0.5) / precision)
end
local TemperaturePrecision = 1
-- convert thermostat format (C) to local format (C or F)
local function localizeTemp(temperature)
return (veraTemperatureScale == "C") and round(temperature, TemperaturePrecision) or
round(((temperature + 0.0) * 1.8) + 32, TemperaturePrecision)
end
-- convert local format (C or F) to thermostat format (C)
local function delocalizeTemp(temperature)
return (veraTemperatureScale == "C") and temperature or
round(((temperature + 0.0) - 32.0) / 1.8, 1000)
end
-- convert Nest values to UPnP values
local NEST_TO_UPNP = {
[NEST_SID] = {
},
[NEST_STRUCTURE_SID] = {
["StreetAddress"] = function(structure) return structure.street_address or "" end,
["Location"] = function(structure) return structure.location or "" end,
["PostalCode"] = function(structure) return structure.postal_code or "" end,
["CountryCode"] = function(structure) return structure.country_code or "" end
},
[HOUSE_STATUS_SID] = {
["OccupancyState"] = function(structure) return structure.away and "Unoccupied" or "Occupied" end
},
[SWITCH_POWER_SID] = {
["Status"] = function(structure) return structure.away and "0" or "1" end
},
[TEMP_SENSOR_SID] = {
["Application"] = function() return "Room" end,
["CurrentTemperature"] = function(shared, device) return tostring(localizeTemp(shared.current_temperature)) end
},
[TEMP_SETPOINT_HEAT_SID] = {
["Application"] = function() return "Heating" end,
["CurrentSetpoint"] = function(shared, device)
return (shared.target_temperature_type == "heat") and
tostring(localizeTemp(shared.target_temperature)) or
tostring(localizeTemp(shared.target_temperature_low))
end
},
[TEMP_SETPOINT_COOL_SID] = {
["Application"] = function() return "Cooling" end,
["CurrentSetpoint"] = function(shared, device)
return (shared.target_temperature_type == "cool") and
tostring(localizeTemp(shared.target_temperature)) or
tostring(localizeTemp(shared.target_temperature_high))
end
},
[HUMIDITY_SENSOR_SID] = {
["CurrentLevel"] = function(shared, device) return tostring(device.current_humidity) end
},
[HVAC_FAN_SID] = {
["Mode"] = function(shared, device)
if not device.has_fan then return "Off"
elseif (device.fan_mode == "on") then return "ContinuousOn"
elseif (device.fan_mode == "auto") then return "Auto"
else return "Auto"
end
end,
["FanStatus"] = function(shared, device) return shared.hvac_fan_state and "On" or "Off" end
},
[HVAC_USER_SID] = {
["ModeStatus"] = function(shared, device)
if (shared.target_temperature_type == "heat") then return "HeatOn"
elseif (shared.target_temperature_type == "cool") then return "CoolOn"
elseif (shared.target_temperature_type == "range") then return "AutoChangeOver"
elseif (shared.target_temperature_type == "off") then return "Off"
else return "InDeadBand"
end
end
},
[HVAC_STATE_SID] = {
["ModeState"] = function(shared, device)
if (device.switch_system_off) then return "Off"
elseif (shared.hvac_heater_state) then return "Heating"
elseif (shared.hvac_ac_state) then return "Cooling"
elseif (shared.hvac_fan_state) then return "FanOnly"
else return "Idle"
end
end
},
[HA_DEVICE_SID] = {
["Commands"] = function(shared, device)
local commands = "hvac_off,hvac_auto,hvac_state,heating_setpoint,cooling_setpoint"
if shared.can_heat then
commands = commands .. ",hvac_heat"
end
if shared.can_cool then
commands = commands .. ",hvac_cool"
end
if device.has_fan then
commands = commands .. ",fan_label,fan_auto,fan_on"
end
return commands
end,
["BatteryLevel"] = function(shared, device, topaz)
if topaz then
local millivolts = tonumber(topaz.battery_level)
if (millivolts == nil) then return "0"
else
millivolts = (millivolts < PROTECT_BATTERY_LOW) and PROTECT_BATTERY_LOW or millivolts
millivolts = (millivolts > PROTECT_BATTERY_HIGH) and PROTECT_BATTERY_HIGH or millivolts
local percent = ((millivolts - PROTECT_BATTERY_LOW) * 100.0) / (PROTECT_BATTERY_HIGH - PROTECT_BATTERY_LOW)
return tostring(math.floor(percent))
end
else
local volts = tonumber(device.battery_level)
if (volts == nil) then return "0"
else
volts = (volts < NEST_BATTERY_LOW) and NEST_BATTERY_LOW or volts
volts = (volts > NEST_BATTERY_HIGH) and NEST_BATTERY_HIGH or volts
local percent = ((volts - NEST_BATTERY_LOW) * 100.0) / (NEST_BATTERY_HIGH - NEST_BATTERY_LOW)
return tostring(math.floor(percent))
end
end
end,
["BatteryDate"] = function(track, topaz)
return tostring(math.floor((topaz and topaz["$timestamp"] or track.last_connection) / 1000))
end,
["LastUpdate"] = function(track, structure, topaz)
if (type(track) == "table") then return tostring(math.floor(track.last_connection / 1000))
elseif (type(structure) == "table") then return tostring(math.floor(structure["$timestamp"] / 1000))
elseif (type(topaz) == "table") then return tostring(math.floor(topaz["$timestamp"] / 1000))
end
end
},
[MCV_ENERGY_METERING_SID] = {
["UserSuppliedWattage"] = function(shared, device) return "0,0,0" end
},
[SECURITY_SENSOR_SID] = {
["Tripped"] = function(topaz, sensorType)
if sensorType == SMOKE_ID_PREFIX then return topaz.smoke_status == 0 and "0" or "1"
elseif sensorType == CO_ID_PREFIX then return topaz.co_status == 0 and "0" or "1"
end
end,
["LastTrip"] = function(topaz, sensorType) return "0"
end,
["Armed"] = function(topaz, sensorType) return "0"
end
}
}
local function nestToUpnp(serviceId, variableName, ...)
return NEST_TO_UPNP[serviceId][variableName](...)
end
local function nestToUpnpParam(serviceId, variableName, ...)
return serviceId .. "," .. variableName .. "=" .. nestToUpnp(serviceId, variableName, ...)
end
local function writeVariableFromNest(lul_device, serviceId, name, ...)
writeVariable(lul_device, serviceId, name, nestToUpnp(serviceId, name, ...))
end
local function writeVariableFromNestIfChanged(lul_device, serviceId, name, ...)
return writeVariableIfChanged(lul_device, serviceId, name, nestToUpnp(serviceId, name, ...))
end
-- convert UPnP values to Nest values
local UPNP_TO_NEST = {
[HVAC_USER_SID] = {
["ModeTarget"] = function(ModeTarget)
if (ModeTarget == "HeatOn") then return "heat"
elseif (ModeTarget == "CoolOn") then return "cool"
elseif (ModeTarget == "AutoChangeOver") then return "range"
elseif (ModeTarget == "Off") then return "off"
end
end
}
}
local function upnpToNest(serviceId, variableName, ...)
return UPNP_TO_NEST[serviceId][variableName](...)
end
local function readSettings()
local data = {}
-- Config variables
data.username = readVariableOrInit(PARENT_DEVICE, NEST_SID, "username", "" )
data.password = readVariableOrInit(PARENT_DEVICE, NEST_SID, "password", "" )
data.period = tonumber(readVariableOrInit(PARENT_DEVICE, NEST_SID, "period", tostring(DEFAULT_POLL)))
data.period = data.period or DEFAULT_POLL
data.period = (data.period < MIN_POLL) and MIN_POLL or data.period
-- Internal variables
data.transport_url = readVariableOrInit(PARENT_DEVICE, NEST_SID, "transport_url", "")
data.userid = readVariableOrInit(PARENT_DEVICE, NEST_SID, "userid", "")
data.access_token = readVariableOrInit(PARENT_DEVICE, NEST_SID, "access_token", "")
return data
end
local function saveSession(transport_url, userid, access_token)
writeVariable(PARENT_DEVICE, NEST_SID, "transport_url", transport_url)
writeVariable(PARENT_DEVICE, NEST_SID, "userid", userid)
writeVariable(PARENT_DEVICE, NEST_SID, "access_token", access_token)
local status = (transport_url ~= "" and userid ~= "" and access_token ~= "")
writeVariable(PARENT_DEVICE, NEST_SID, "status", status and "1" or "0")
end
local function clearSession()
saveSession("", "", "")
end
local function makeWhereMap(res)
local whereMap = {}
if res.where then
for k,v in pairs(res.where) do
if v.wheres then
for i,entry in ipairs(v.wheres) do
whereMap[entry.where_id] = entry.name
end
end
end
end
return whereMap
end
local function getThermostatName(shared, device, whereMap)
local name = shared.name or ""
local label = (device.where_id and whereMap[device.where_id]) and whereMap[device.where_id] or ""
local parens = (name ~= "" and label ~= "")
return name .. (parens and " (" or "") .. label .. (parens and ")" or "")
end
local function getProtectName(topaz, whereMap)
local name = topaz.description or ""
local label = (topaz.where_id and whereMap[topaz.where_id]) and whereMap[topaz.where_id] or ""
local parens = (name ~= "" and label ~= "")
return name .. (parens and " (" or "") .. label .. (parens and ")" or "")
end
-- login to nest.com if we think we need to
-- return a loginSettings table on success or nil
local function login()
-- debug("in login()")
local data = readSettings()
-- if there is no username or password, then no matter what, don't proceed
if (data.username == "" or data.password == "") then
task("Please specify username and password.")
clearSession()
return
end
-- if there is old session data, it might still be good, so try it!
if (data.transport_url ~= "" and data.userid ~= "" and data.access_token ~= "") then
debug("assuming saved login credentials are still valid.")
return data
end
-- perform https POST to nest.com to login
local res = {}
local body = urlcode.encodetable{username = data.username, password = data.password}
local headers = {["user-agent"] = NEST_UA,
["content-length"] = string.len(body),
["content-type"] = "application/x-www-form-urlencoded"}
local url = { url = "https://home.nest.com/user/login",
protocol = "tlsv1",
method = "POST",
source = ltn12.source.string(body),
sink = ltn12.sink.table(res),
headers = headers }
local one, code, headers, status = https.request(url)
if (code == 200) then
res = json.decode(table.concat(res))
data.transport_url = res.urls.transport_url
data.userid = res.userid
data.access_token = res.access_token
task("Successful nest.com login.", TASK_SUCCESS)
saveSession(data.transport_url, data.userid, data.access_token)
return data
else
task("Failed to login: code=" .. tostring(code) .. ", status=" .. tostring(status))
clearSession()
end
end
local statusOutstanding = false
function getStatus()
-- debug("in getStatus()")
statusOutstanding = false
local session = login()
if (session ~= nil) then
-- perform https GET to nest.com to get status
local res = {}
local headers = {["user-agent"] = NEST_UA,
["Authorization"] = "Basic " .. session.access_token,
["X-nl-user-id"] = session.userid,
["X-nl-protocol-version"] = "1"}
local url = { url = session.transport_url .. "/v2/mobile/user." .. session.userid,
protocol = "tlsv1",
method = "GET",
sink = ltn12.sink.table(res),
headers = headers }
local one, code, headers, status = https.request(url)
res = json.decode(table.concat(res))
if (code ~= 200) or type(res) ~= "table" or not (res.shared or res.topaz) then
log("error getting status from nest.com with HTTP code=" .. tostring(code) .. ", status=" .. tostring(status))
-- assume there is something wrong with the session
clearSession()
else
debug("success getting status from nest.com")
local whereMap = makeWhereMap(res)
-- create any missing structure, thermostat, humidistat, smoke and CO children devices, remove any wrong ones
local added = 0
local ptr = luup.chdev.start(PARENT_DEVICE)
local isUI7 = luup.variable_get(NEST_SID, "UI7Check", PARENT_DEVICE) == "true"
local suffix = isUI7 and "_UI7" or ""
if res.structure then
for id,structure in pairs(res.structure) do
local altid = STRUCTURE_ID_PREFIX .. id
if (findChild(PARENT_DEVICE, altid) == nil) then
added = added + 1
end
luup.chdev.append(PARENT_DEVICE, ptr, altid, structure.name, "urn:schemas-watou-com:device:NestStructure:1",
"D_NestStructure1" .. suffix .. ".xml", "",
nestToUpnpParam(HOUSE_STATUS_SID, "OccupancyState", structure) ..
"\n" .. nestToUpnpParam(NEST_STRUCTURE_SID, "StreetAddress", structure) ..
"\n" .. nestToUpnpParam(NEST_STRUCTURE_SID, "Location", structure) ..
"\n" .. nestToUpnpParam(NEST_STRUCTURE_SID, "PostalCode", structure) ..
"\n" .. nestToUpnpParam(NEST_STRUCTURE_SID, "CountryCode", structure) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "LastUpdate", nil, structure) ..
"\n" .. nestToUpnpParam(SWITCH_POWER_SID, "Status", structure)
, false, false)
end
end
if res.shared then
for id,shared in pairs(res.shared) do
local altid = THERMOSTAT_ID_PREFIX .. id
if (findChild(PARENT_DEVICE, altid) == nil) then
added = added + 1
end
local device = res.device[id]
local track = res.track[id]
luup.chdev.append(PARENT_DEVICE, ptr, altid, getThermostatName(shared, device, whereMap), "urn:schemas-watou-com:device:HVAC_ZoneThermostat:1",
"D_NestThermostat1" .. suffix .. ".xml", "",
nestToUpnpParam(TEMP_SENSOR_SID, "CurrentTemperature", shared, device) ..
"\n" .. nestToUpnpParam(TEMP_SETPOINT_HEAT_SID, "CurrentSetpoint", shared, device) ..
"\n" .. nestToUpnpParam(TEMP_SETPOINT_COOL_SID, "CurrentSetpoint", shared, device) ..
"\n" .. nestToUpnpParam(HVAC_FAN_SID, "Mode", shared, device) ..
"\n" .. nestToUpnpParam(HVAC_FAN_SID, "FanStatus", shared, device) ..
"\n" .. nestToUpnpParam(HVAC_USER_SID, "ModeStatus", shared, device) ..
"\n" .. nestToUpnpParam(HVAC_STATE_SID, "ModeState", shared, device) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "Commands", shared, device) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "BatteryLevel", shared, device) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "BatteryDate", track) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "LastUpdate", track) ..
"\n" .. nestToUpnpParam(MCV_ENERGY_METERING_SID, "UserSuppliedWattage", shared, device)
, false, false)
end
for id,shared in pairs(res.shared) do
local altid = HUMIDISTAT_ID_PREFIX .. id
if (findChild(PARENT_DEVICE, altid) == nil) then
added = added + 1
end
local device = res.device[id]
local track = res.track[id]
luup.chdev.append(PARENT_DEVICE, ptr, altid, getThermostatName(shared, device, whereMap) .. " Humidity", "urn:schemas-watou-com:device:NestHumidistat:1",
"D_NestHumidistat1" .. suffix .. ".xml", "",
nestToUpnpParam(HUMIDITY_SENSOR_SID, "CurrentLevel", shared, device) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "LastUpdate", track)
, false, false)
end
end
-- add any smoke and CO detectors
if res.topaz then
for id,topaz in pairs(res.topaz) do
local altid = SMOKE_ID_PREFIX .. id
if (findChild(PARENT_DEVICE, altid) == nil) then
added = added + 1
end
luup.chdev.append(PARENT_DEVICE, ptr, altid, getProtectName(topaz, whereMap) .. " Smoke", "urn:schemas-micasaverde-com:device:SmokeSensor:1",
"D_SmokeSensor1.xml", "",
nestToUpnpParam(SECURITY_SENSOR_SID, "Tripped", topaz, SMOKE_ID_PREFIX) ..
"\n" .. nestToUpnpParam(SECURITY_SENSOR_SID, "LastTrip", topaz, SMOKE_ID_PREFIX) ..
"\n" .. nestToUpnpParam(SECURITY_SENSOR_SID, "Armed", topaz, SMOKE_ID_PREFIX) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "BatteryLevel", nil, nil, topaz) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "BatteryDate", nil, topaz) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "LastUpdate", nil, nil, topaz)
, false, false)
altid = CO_ID_PREFIX .. id
if (findChild(PARENT_DEVICE, altid) == nil) then
added = added + 1
end
luup.chdev.append(PARENT_DEVICE, ptr, altid, getProtectName(topaz, whereMap) .. " CO", isUI7 and "urn:schemas-micasaverde-com:device:SmokeSensor:1" or "urn:schemas-watou-com:device:NestCOSensor:1",
isUI7 and "D_SmokeSensor1.xml" or "D_NestCOSensor1.xml", "",
nestToUpnpParam(SECURITY_SENSOR_SID, "Tripped", topaz, CO_ID_PREFIX) ..
"\n" .. nestToUpnpParam(SECURITY_SENSOR_SID, "LastTrip", topaz, CO_ID_PREFIX) ..
"\n" .. nestToUpnpParam(SECURITY_SENSOR_SID, "Armed", topaz, CO_ID_PREFIX) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "BatteryLevel", nil, nil, topaz) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "BatteryDate", nil, topaz) ..
"\n" .. nestToUpnpParam(HA_DEVICE_SID, "LastUpdate", nil, nil, topaz)
, false, false)
end
end
luup.chdev.sync(PARENT_DEVICE, ptr)
if (added > 0) then
debug("Added " .. added .. " children device(s); awaiting restart...")
return
end
-- update all structure, thermostat, humidistat, smoke and CO children devices
if res.structure then
for id,structure in pairs(res.structure) do
local altid = STRUCTURE_ID_PREFIX .. id
local child = findChild(PARENT_DEVICE, altid)
if (child == nil) then
log("failed to find device for structure " .. altid)
else
-- make sure this device has category_num=5
if (luup.attr_get("category_num", child) ~= "5") then
luup.attr_set("category_num", "5", child)
end
writeVariableFromNestIfChanged(child, HOUSE_STATUS_SID, "OccupancyState", structure)
writeVariableFromNestIfChanged(child, SWITCH_POWER_SID, "Status", structure)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "LastUpdate", nil, structure)
end
end
end
if res.shared then
for id,shared in pairs(res.shared) do
local altid = THERMOSTAT_ID_PREFIX .. id
local child = findChild(PARENT_DEVICE, altid)
if (child == nil) then
log("failed to find device for thermostat " .. altid)
else
-- make sure this device has category_num=5
if (luup.attr_get("category_num", child) ~= "5") then
luup.attr_set("category_num", "5", child)
end
local device = res.device[id]
local track = res.track[id]
writeVariableFromNestIfChanged(child, TEMP_SENSOR_SID, "CurrentTemperature", shared, device)
writeVariableFromNestIfChanged(child, TEMP_SETPOINT_HEAT_SID, "CurrentSetpoint", shared, device)
writeVariableFromNestIfChanged(child, TEMP_SETPOINT_COOL_SID, "CurrentSetpoint", shared, device)
writeVariableFromNestIfChanged(child, HVAC_FAN_SID, "Mode", shared, device)
writeVariableFromNestIfChanged(child, HVAC_FAN_SID, "FanStatus", shared, device)
writeVariableFromNestIfChanged(child, HVAC_USER_SID, "ModeStatus", shared, device)
writeVariableFromNestIfChanged(child, HVAC_STATE_SID, "ModeState", shared, device)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "Commands", shared, device)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "BatteryLevel", shared, device)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "BatteryDate", track)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "LastUpdate", track)
end
end
for id,shared in pairs(res.shared) do
local altid = HUMIDISTAT_ID_PREFIX .. id
local child = findChild(PARENT_DEVICE, altid)
if (child == nil) then
log("failed to find device for humidistat " .. altid)
else
-- make sure this device has category_num=16
if (luup.attr_get("category_num", child) ~= "16") then
luup.attr_set("category_num", "16", child)
end
local device = res.device[id]
local track = res.track[id]
writeVariableFromNestIfChanged(child, HUMIDITY_SENSOR_SID, "CurrentLevel", shared, device)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "LastUpdate", track)
end
end
end
if res.topaz then
for id,topaz in pairs(res.topaz) do
local altid = SMOKE_ID_PREFIX .. id
local child = findChild(PARENT_DEVICE, altid)
if (child == nil) then
log("failed to find device for smoke detector " .. altid)
else
-- make sure this device has category_num=4 (Security Sensor)
if (luup.attr_get("category_num", child) ~= "4") then
luup.attr_set("category_num", "4", child)
end
-- make sure this device has subcategory_num=4 (Smoke Sensor)
if (luup.attr_get("subcategory_num", child) ~= "4") then
luup.attr_set("subcategory_num", "4", child)
end
-- set LastTrip to now if Tripped is transitioning from "0" to "1"
local newTripped = nestToUpnp(SECURITY_SENSOR_SID, "Tripped", topaz, SMOKE_ID_PREFIX)
if newTripped == "1" and luup.variable_get(SECURITY_SENSOR_SID, "Tripped", child) ~= "1" then
writeVariableIfChanged(child, SECURITY_SENSOR_SID, "LastTrip", tostring(os.time()))
end
writeVariableIfChanged(child, SECURITY_SENSOR_SID, "Tripped", newTripped)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "BatteryLevel", nil, nil, topaz)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "BatteryDate", nil, topaz)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "LastUpdate", nil, nil, topaz)
end
altid = CO_ID_PREFIX .. id
local child = findChild(PARENT_DEVICE, altid)
if (child == nil) then
log("failed to find device for CO detector " .. altid)
else
-- make sure this device has category_num=4 (Security Sensor)
if (luup.attr_get("category_num", child) ~= "4") then
luup.attr_set("category_num", "4", child)
end
-- make sure this device has subcategory_num=5 (CO Sensor)
if (luup.attr_get("subcategory_num", child) ~= "5") then
luup.attr_set("subcategory_num", "5", child)
end
-- set LastTrip to now if Tripped is transitioning from "0" to "1"
local newTripped = nestToUpnp(SECURITY_SENSOR_SID, "Tripped", topaz, CO_ID_PREFIX)
if newTripped == "1" and luup.variable_get(SECURITY_SENSOR_SID, "Tripped", child) ~= "1" then
writeVariableIfChanged(child, SECURITY_SENSOR_SID, "LastTrip", tostring(os.time()))
end
writeVariableIfChanged(child, SECURITY_SENSOR_SID, "Tripped", newTripped)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "BatteryLevel", nil, nil, topaz)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "BatteryDate", nil, topaz)
writeVariableFromNestIfChanged(child, HA_DEVICE_SID, "LastUpdate", nil, nil, topaz)
end
end
end
end
end
end
local function setAway(structure_id, away)
debug("in setAway()")
local session = login()
if (session == nil) then
return false
else
local res = {}
local data = '{"away_timestamp":' .. tostring(os.time()) .. '000,"away":' .. tostring(away) .. ',"away_setter":0}'
local headers = {["user-agent"] = NEST_UA,
["Authorization"] = "Basic " .. session.access_token,
["X-nl-protocol-version"] = "1",
["content-length"] = string.len(data),
["content-type"] = "application/json"}
local url = { url = session.transport_url .. "/v2/put/structure." .. structure_id,
protocol = "tlsv1",
method = "POST",
source = ltn12.source.string(data),
sink = ltn12.sink.table(res),
headers = headers }
-- local https = require("ssl.https")
local one, code, headers, status = https.request(url)
if code == 302 then
debug("redirecting...")
if headers.location then
url.url = headers.location
url.source = ltn12.source.string(data)
url.create = nil
one, code, headers, status = https.request(url)
else
log("asked to redirect but no location given")
end
end
return code == 200
end
end
local function setTargetTemperature(serviceId, lul_device, tempC)
debug("in setTargetTemperature()")
local session = login()
if (session == nil) then
return false
else
local status = luup.variable_get(HVAC_USER_SID, "ModeStatus", lul_device)
local target = (serviceId == TEMP_SETPOINT_HEAT_SID) and
((status == "HeatOn") and "target_temperature" or "target_temperature_low") or
((status == "CoolOn") and "target_temperature" or "target_temperature_high")
local thermostat_id = getThermostatId(luup.devices[lul_device].id)
local res = {}
local data = '{"target_change_pending":true,"' .. target .. '":' .. string.format('%0.1f', tempC) .. '}'
local headers = {["user-agent"] = NEST_UA,
["Authorization"] = "Basic " .. session.access_token,
["X-nl-protocol-version"] = "1",
["content-length"] = string.len(data),
["content-type"] = "application/json"}
local url = { url = session.transport_url .. "/v2/put/shared." .. thermostat_id,
protocol = "tlsv1",
method = "POST",
source = ltn12.source.string(data),
sink = ltn12.sink.table(res),
headers = headers }
local one, code, headers, status = https.request(url)
if code == 302 then
debug("redirecting...")
if headers.location then
url.url = headers.location
url.source = ltn12.source.string(data)
url.create = nil
one, code, headers, status = https.request(url)
else
log("asked to redirect but no location given")
end
end
return code == 200
end
end
local function setTargetTemperatureType(thermostat_id, targetType)
debug("in setTargetTemperatureType()")
if (targetType ~= "off" and targetType ~= "range" and
targetType ~= "heat" and targetType ~= "cool") then
log("Invalid target_temperature_type: " .. tostring(targetType))
return false
end
local session = login()
if (session == nil) then
return false
else
local res = {}
local data = '{"target_temperature_type":"' .. targetType .. '"}'
local headers = {["user-agent"] = NEST_UA,
["Authorization"] = "Basic " .. session.access_token,
["X-nl-protocol-version"] = "1",
["content-length"] = string.len(data),
["content-type"] = "application/json"}
local url = { url = session.transport_url .. "/v2/put/shared." .. thermostat_id,
protocol = "tlsv1",
method = "POST",
source = ltn12.source.string(data),
sink = ltn12.sink.table(res),
headers = headers }
local one, code, headers, status = https.request(url)
if code == 302 then
debug("redirecting...")
if headers.location then
url.url = headers.location
url.source = ltn12.source.string(data)
url.create = nil
one, code, headers, status = https.request(url)
else
log("asked to redirect but no location given")
end
end
return code == 200
end
end
local function setFan(thermostat_id, mode)
debug("in setFan()")
local session = login()
if (session == nil) then
return false
else
local res = {}
local data = '{"fan_mode":"' .. mode .. '"}'
local headers = {["user-agent"] = NEST_UA,
["Authorization"] = "Basic " .. session.access_token,
["X-nl-protocol-version"] = "1",
["content-length"] = string.len(data),
["content-type"] = "application/json"}
local url = { url = session.transport_url .. "/v2/put/device." .. thermostat_id,
protocol = "tlsv1",
method = "POST",
source = ltn12.source.string(data),
sink = ltn12.sink.table(res),
headers = headers }
local one, code, headers, status = https.request(url)
if code == 302 then
debug("redirecting...")
if headers.location then
url.url = headers.location
url.source = ltn12.source.string(data)
url.create = nil
one, code, headers, status = https.request(url)
else
log("asked to redirect but no location given")
end
end
return code == 200
end
end
function poll()
-- debug("in poll()")
task("Clearing...", TASK_SUCCESS)
getStatus()
-- set up the next poll
local period = readVariableOrInit(PARENT_DEVICE, NEST_SID, "period", tostring(DEFAULT_POLL))
debug("polling device " .. PARENT_DEVICE .. " again in " .. period .. " seconds")
luup.call_timer("poll", 1, period, "", "")
end
local function getStatusSoon()
if (not statusOutstanding) then
luup.call_timer("getStatus", 1, SOON, "", "")
statusOutstanding = true
end
end
local function checkVersion()
local ui7Check = luup.variable_get(NEST_SID, "UI7Check", PARENT_DEVICE) or ""
if ui7Check == "" then
ui7Check = "false"
luup.variable_set(NEST_SID, "UI7Check", ui7Check, PARENT_DEVICE)
end
if (luup.version_branch == 1 and luup.version_major == 7 and ui7Check == "false") then
luup.variable_set(NEST_SID, "UI7Check", "true", PARENT_DEVICE)
luup.attr_set("device_json", "D_Nest1_UI7.json", PARENT_DEVICE)
luup.reload()
end
return true
end
function init(lul_device)
log("plugin version " .. PLUGIN_VERSION .. " starting up...", 50)
PARENT_DEVICE = lul_device
if not checkVersion() then
return false
end
Logging = tonumber(readVariableOrInit(PARENT_DEVICE, NEST_SID, "Logging", "0"))
Logging = Logging or 0
if Logging < 0 or Logging > 1 then
Logging = 0
end
debug("Logging is set to " .. Logging)
TemperaturePrecision = tonumber(readVariableOrInit(PARENT_DEVICE, NEST_SID, "TemperatureScale", "1"))
TemperaturePrecision = TemperaturePrecision or 1
if TemperaturePrecision < 1 or TemperaturePrecision > 1000 then
TemperaturePrecision = 1
end
getVeraTemperatureScale()
https = require("ssl.https")
urlcode = require("L_nest_urlcode")
require("ltn12")
debug("polling device " .. PARENT_DEVICE .. " in " .. SOON .. " seconds")
luup.call_timer("poll", 1, SOON, "", "")
end
</functions>
<startup>init</startup>
<actionList>
<action>
<serviceId>urn:watou-com:serviceId:Nest1</serviceId>
<name>SetUsername</name>
<run>
luup.variable_set(NEST_SID, "username", lul_settings.username or "", lul_device)
clearSession()
getStatusSoon()
</run>
</action>
<action>
<serviceId>urn:watou-com:serviceId:Nest1</serviceId>
<name>GetUsername</name>
<run>
return luup.variable_get(NEST_SID, "username", lul_device)
</run>
</action>
<action>
<serviceId>urn:watou-com:serviceId:Nest1</serviceId>
<name>SetPassword</name>
<run>
luup.variable_set(NEST_SID, "password", lul_settings.password or "", lul_device)
clearSession()
getStatusSoon()
</run>
</action>
<action>
<serviceId>urn:watou-com:serviceId:Nest1</serviceId>
<name>GetPassword</name>
<run>
return luup.variable_get(NEST_SID, "password", lul_device)
</run>
</action>
<action>
<serviceId>urn:watou-com:serviceId:Nest1</serviceId>
<name>SetPeriod</name>
<run>
local period = tonumber(lul_settings.period)
period = period or DEFAULT_POLL
period = (period < MIN_POLL) and MIN_POLL or period