-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathL_Netatmo.lua
1069 lines (913 loc) · 39.9 KB
/
L_Netatmo.lua
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
--module ("L_Netatmo", package.seeall) -- for debug only
ABOUT = {
NAME = "Netatmo",
VERSION = "2023.01.08",
DESCRIPTION = "Netatmo plugin - Virtual sensors for all your Netatmo Weather Station devices and modules",
AUTHOR = "@akbooer",
COPYRIGHT = "(c) 2013-2023 AKBooer",
DOCUMENTATION = "https://github.com/akbooer/Netatmo/tree/master/",
LICENSE = [[
Copyright 2013-2023 AK Booer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
}
------------------------------------------------------------------------
--
-- NETATMO Vera plugin
--
-- Virtual sensors for all your Netatmo Weather Station devices and modules (
-- temperature, humidity, pressure, CO2, noise & rainfall.)
-- @akbooer 2013-2017/...
--
-- inspired by the excellent web tutorial by Sébastien Joly "Collecter les données d'une station Netatmo":
-- http://www.domotique-info.fr/2013/05/collecter-les-donnees-dune-station-netatmo-depuis-une-vera-tuto/
--
-- June 2014 - refactored to use updated API
-- special thanks to @Kullematz for fast-turnaround beta testing.
-- and to @reneboer for fixing the UI7 "Can't Detect Device" error,
-- see: http://forum.micasaverde.com/index.php/topic,16276.msg203653.html#msg203653
-- AND for fixing the UI7 .json files http://forum.micasaverde.com/index.php/topic,16276.msg203683.html#msg203683
------------------------------------------------------------------------
--
-- 20-Aug-2013, Beta 0.1: http://forum.micasaverde.com/index.php/topic,13489.msg123451.html#msg123451
-- 27-Aug-2013, Beta 0.2: http://forum.micasaverde.com/index.php/topic,16276.msg124383.html#msg124383
-- 30-Aug-2013, Beta 0.3: http://forum.micasaverde.com/index.php/topic,16276.msg124705.html#msg124705
-- 03-Sep-2013, Release 1: http://forum.micasaverde.com/index.php/topic,16276.msg125175.html#msg125175
-- 17-Sep-2013, Patch 1: http://forum.micasaverde.com/index.php/topic,16276.msg127541.html#msg127541
-- 21-Sep-2013, Beta 1.1: http://forum.micasaverde.com/index.php/topic,16276.msg127886.html#msg127886
-- 23-Sep-2013, Beta 1.2: http://forum.micasaverde.com/index.php/topic,16276.msg128194.html#msg128194
-- 05-Nov-2013, Release 2: http://forum.micasaverde.com/index.php/topic,16276.msg135899.html#msg135899
-- 18-Dec-2013, Beta 2.1: http://forum.micasaverde.com/index.php/topic,16276.msg142231.html#msg142231
-- 24-Apr-2014, Beta 2.2: http://forum.micasaverde.com/index.php/topic,16276.msg173122.html#msg173122
-- 09-Jun-2014, New API: http://forum.micasaverde.com/index.php/topic,16276.msg179917.html#msg179917
-- 12-Nov-2014, Release 3: http://forum.micasaverde.com/index.php/topic,16276.msg203099.html#msg203099
-- Beta 3.1: UI7 fixes
-- 01-Feb-2015, add hack to remove "<br>" from device variable value
-- 02-Feb-2015, remove history measurement cache and plotting capability (best done elsewhere)
-- 27-Jul-2015, add ALTUI compatibility with formatted display line
-- Beta 3.2: New API (another one)
-- 04-Dec-2015, add support for new wind module
-- 14-Jan-2016, further use of new API and single device type NetatmoMetric for all non-standard metrics
-- 20-Jan-2016, Release 4: candidate
-- 26-Jan-2016, fix wind child units problem, thanks @korttoma (and for the use of your wind gauge)
-- see: http://forum.micasaverde.com/index.php/topic,35162.msg266522.html#msg266522
--
-- 2017.01.16 fix icon name typo in setMetricIcon, thanks @reneboer
-- 2017.02.14 fix nil value error in format
-- 2018.02.27 add ABOUT global with VERSION for openLuup Plugins page
-- 2019.01.30 move HTTP handler startup to earlier in init code (debug info available)
-- 2019.12.16 fix error in D_NetatmoMetric.xml, update .json file to point to CDN for online icons
-- 2020.01.09 update D_Netatmo.json to use CDN
-- 2020.05.11 quick fix to create 'missing' devices (not found in current modules)
-- 2020.10.15 fix possibly missing "station_name" / "home_name" (thanks @Krisztian_Szabo)
-- 2020.10.21 another fix for the above (with multiple stations) (thanks again @Krisztian_Szabo)
-- remove CLI library
-- 2021.04.20 use openLuup.json, if present (it may use Cjson or RapidJSON)
-- 2022.12.30 changes due to deprecated Oath2 Client Credentials username/password login (thanks @mrFarmer aka @reneboer)
-- see: https://smarthome.community/topic/993/netatmo-oath2-login/20
-- 2023.01.06 Use Oath2 Token Authorization - no need for individual user-created app on Netatmo site
-- 2023.01.08 Changes to work on Vera too!
local socket = require "socket"
local https = require "ssl.https"
local library = require "L_Netatmo2"
local is_openLuup = luup.openLuup
local json = is_openLuup and require "openLuup.json" or library.json()
local gviz = library.gviz()
local Netatmo -- Netatmo API object with access methods
local Timestamp -- last update
local stationInfo -- table with latest Netatmo device configuration
-- ServiceId strings for the different sensors
local NetatmoSID = "urn:akbooer-com:serviceId:Netatmo1"
local tempSID = "urn:upnp-org:serviceId:TemperatureSensor1"
local humidSID = "urn:micasaverde-com:serviceId:HumiditySensor1"
local genericSID = "urn:micasaverde-com:serviceId:GenericSensor1"
local altuiSID = "urn:upnp-org:serviceId:altui1" -- Variables = 'DisplayLine1' and 'DisplayLine2'
local batterySID = "urn:micasaverde-com:serviceId:HaDevice1" -- 'BatteryLevel'
-- Device files for the different sensors
local tempXML = "D_TemperatureSensor1.xml"
local humidXML = "D_HumiditySensor1.xml"
local genericXML = "D_NetatmoMetric.xml"
-- redirect URI for authorization
local function myIP ()
local mySocket = socket.udp ()
mySocket:setpeername ("42.42.42.42", 42) -- arbitrary IP and PORT
local ip = mySocket:getsockname ()
mySocket: close()
return ip or "127.0.0.1"
end
local myPORT = is_openLuup and ':' or "/port_" -- required for Vera functionality
local redirect_uri = table.concat {"http://", myIP(), myPORT, "3480/data_request?id=lr_Netatmo"}
local state_parameter = tostring {} -- unique state parameter string
-- shorthand for the measurements
local T,H,C,P,N,R,W = "Temperature", "Humidity", "CO2", "Pressure", "Noise", "Rain", "WindStrength"
local THCPNRW = {T=T, H=H, C=C, P=P, N=N, R=R, W=W} -- lookup table for user-defined sensor types
local metric_types = -- these can all be written to child devices
{
[T] = {T, "MinTemp", "MaxTemp", "DewPoint"},
[H] = {H},
[C] = {C},
[P] = {P, "AbsolutePressure"},
[N] = {N},
[R] = {R, "SumRain1", "SumRain24"},
[W] = {W, "GustStrength", "MaxWindStr"},
}
local type_of_metric = {} -- reverse lookup table for metric type
for t, types in pairs(metric_types) do
for _,name in ipairs (types) do
type_of_metric [name] = t
end
end
-- Luup variable names for Child devices
local unitsVariable = "Units" -- measurement units
local calibrationOffset = "CalibrationOffset" -- calibration parameter
local iconsVariable = "IconSet" -- which icons to use
local dateDisplay = "DateDisplay" -- the formatted measurement date/time
local dateFormat = "DateFormat" -- the os.date style format for the above
-- for writing to Luup variables, need serviceId and variable name for each sensor type
-- for creating child devices also need device xml filename
local LuupInfo = setmetatable (
{
[T] = { deviceXML = tempXML, service = tempSID, variable = "CurrentTemperature"},
[H] = { deviceXML = humidXML, service = humidSID, variable = "CurrentLevel"}
},
{__index = function () -- default for everything else
return { deviceXML = genericXML, service = genericSID, variable = "CurrentLevel"}
end
}
)
local context = {} -- for debugging, etc.
local ChildDevice = {} -- lookup table: ChildDevice [ChildId] = ChildObject
local metric -- metric handling utilities
local tokenRefresh = 120 -- interval (in minutes) in which to refresh access tokens
---
-- 'global' program variables assigned in init()
local NetatmoID -- Luup device ID
local measurementPoll -- polling frequency (in minutes) for measurement update
local timeFormat -- os.date() compatible format string for timestamp
------------------------------------------------------------------------
--
-- Luup utility routines
--
local function log (message)
luup.log ('Netatmo: '.. (message or '???') )
end
local function get (name, service, device)
local x = luup.variable_get (service or NetatmoSID, name, device or NetatmoID)
return x
end
local function set (name, value, service, device)
service = service or NetatmoSID
device = device or NetatmoID
local old = get (name, service, device)
if tostring(value) ~= old then
luup.variable_set (service, name, value, device)
end
end
-- get and check UI variables
local function uiVar (name, default, lower, upper)
local value = get (name)
local oldvalue = value
if value and (value ~= "") then -- bounds check if required
if lower and (tonumber (value) < lower) then value = lower end
if upper and (tonumber (value) > upper) then value = upper end
else
value = default
end
value = tostring (value)
if value ~= oldvalue then set (name, value) end -- default or limits may have modified value
return value
end
--get device Variables, creating with default value if non-existent
local function devVar (name, default, deviceNo)
local value = get (name, NetatmoSID, deviceNo)
if not value then
value = default -- use default value
set (name, default, NetatmoSID, deviceNo) -- create missing variable with default value
end
return value
end
-- parent device icons
local function setNetatmoIcon (value, display_line)
local index = {clear = 0, blue = 1, green = 2, yellow = 3, red = 4}
display_line = display_line or ''
set ("DisplayLine2", display_line, altuiSID)
if index[value] then set (iconsVariable, index[value]) end
end
-- child device icons THCPNRW
-- corresponding to files NetatmoMetric_0.png, ... NetatmoMetric_250.png
-- to make it work on UI5 and UI7
local function setMetricIcon (value, device)
local icons = {T, H, C, P, N, R, W, "CO2_low", "CO2_med", "CO2_high"}
local index = {generic = 0}
for i,n in ipairs (icons) do index[n] = i end
set (iconsVariable, index[value] or 0, nil, device)
end
------------------------------------------------------------------------
--
-- Dew point calculation using Magnus formula: http://en.wikipedia.org/wiki/Dew_point
--
-- however, @watou uses: local dewpoint = (rh/100)^(1/8) * (112 + (0.9 * t)) - 112 + (0.1 * t)
-- see: https://github.com/watou/lua-weathermetrics/blob/master/weathermetrics.lua
-- ...there is about 0.1 degree difference, at worst, over any reasonable range.
--
local function dewPoint (T, RH)
-- a,b,c taken from a 1980 paper by David Bolton in the Monthly Weather Review
-- local a = 6.112 -- a is not used in this approximation
local b,c = 17.67, 243.5
RH = math.max (RH or 0, 1e-3)
local gamma = math.log (RH/100) + b * T / (c + T)
return c * gamma / (b - gamma)
end
------------------------------------------------------------------------
--
-- METRICS handling
--
-- admin data includes amongst other things:
-- unit (temperature, rain): 0 -> metric system, 1 -> imperial system
-- pressureunit: 0 -> mbar, 1 -> inHg, 2 -> mmHg
-- windunit: 0 -> kph, 1 -> mph, 2 -> m/s, 3 -> beaufort, 4 -> knot
--
local function Metrics (admin)
local function UnitsTable (unit, decimalPlaces, multiplier, offset) -- table constructor
return {unit = unit, dp = decimalPlaces or 0, m = multiplier or 1, c = offset or 0}
end
local unitsLookup = { -- these indices correspond to the Netatmo configuration settings values
[T] = {
[0] = UnitsTable ('°C', 1),
[1] = UnitsTable ('°F', 1, 9/5, 32)
},
[P] = {
[0] = UnitsTable ('mbar', 1),
[1] = UnitsTable ('inHg', 2, 0.0295333727),
[2] = UnitsTable ('mmHg', 1, 0.7500616830)
},
[H] = { [0] = UnitsTable ('%') },
[C] = { [0] = UnitsTable ('ppm') },
[N] = { [0] = UnitsTable ('dB') },
[R] = {
[0] = UnitsTable ('mm', 1),
[1] = UnitsTable ('in', 2, 1 / 25.4),
},
[W] = { -- windunit: 0 -> kph, 1 -> mph, 2 -> m/s, 3 -> beaufort, 4 -> knot
[0] = UnitsTable ('kph', 0),
[1] = UnitsTable ('mph', 0, 0.621371),
[2] = UnitsTable ('m/s', 1, 1000 / 60 /60),
[3] = UnitsTable ('beaufort', 0, 1), -- TODO: beaufort calculation is non-linear: fractional power
-- v = 0.836 B^3/2 m/s, see https://en.wikipedia.org/wiki/Beaufort_scale
[4] = UnitsTable ('knot', 1, 0.539957),
},
}
-- get measurement units from Netatmo settings
-- only Temperature, Pressure, and Rain and Wind configurable at this time
-- (other sensors have 'universal' units of %, ppm, dB)
local units = {}
if admin then -- set T, P, R, and P units to specified units
local temp = admin.unit
log ('admin.unit = ' .. (temp or '?') )
units[R] = unitsLookup [R] [temp or 0]
units[T] = unitsLookup [T] [temp or 0]
local pres = admin.pressureunit
log ('admin.pressureunit = ' .. (pres or '?') )
units[P] = unitsLookup [P] [pres or 0]
local wind = admin.windunit
log ('admin.windunit = ' .. (wind or '?') )
units[W] = unitsLookup [W] [wind or 0]
else
log 'admin data not found'
end
for sensor, info in pairs (unitsLookup) do
units[sensor] = units[sensor] or info[0] -- set all undefined units to default values
end
-- given a metric name, convert to required units, possibly adding offset
local function format (sensor, value, offset)
local localUnit
local conversion = units[type_of_metric[sensor] or '']
if conversion then
localUnit = conversion.unit -- unit converted to local preference
value = (value or 0) * conversion.m + conversion.c + (offset or 0) -- apply units conversion + 2017.02.14
value = ("%0."..conversion.dp.."f"): format (value) -- specify precision for readings
end
return value, localUnit
end
local function unit (m)
local raw_unit, new_unit = ''
local t = type_of_metric[m]
if t then
raw_unit = unitsLookup [t][0].unit or ''
new_unit = units[t].unit or ''
end
return new_unit or raw_unit, raw_unit
end
return {
format = format,
unit = unit,
}
end
------------------------------------------------------------------------
--
-- Netatmo object with basic low-level Netatmo API calls
-- see documentation at: http://dev.netatmo.com/
-- netatmoAPI (client_id, client_secret), netatmo API object constructor with application registration info parameters
-- returns methods to authenticate, refresh tokens, get user options, devices and measurements
local function netatmoAPI (client_id, client_secret)
local access_token, refresh_token -- updated periodically after authorisation
-- HTTPS_request(), HTTPS GET/POST with Lua table body definition and JSON return
-- see http://notebook.kulchenko.com/programming/https-ssl-calls-with-lua-and-luasec
local function HTTPS_request (url, params)
local req, Json
if params then -- it's a POST (otherwise a GET)
req = {}
for name,value in pairs (params) do
req[#req+1] = table.concat {name, '=', value}
end -- build the parameter string
req = table.concat (req,'&')
end
local reply,code = https.request (url, req) -- body, code, headers, status
if code ~= 200 then
log ('HTTPS error = ' .. (code or 'nil'))
-- log (json.encode {request = req})
-- log (json.encode {reply = reply or "---none---"})
Json = {}
else
Json = json.decode (reply)
end
return Json
end
-- authenticate with username/password (deprecated as of October 2022)
local function authenticate (username, password, scope)
scope = scope or "read_station"
local reply = HTTPS_request ("https://api.netatmo.net/oauth2/token",
{
grant_type = "password",
client_id = client_id,
client_secret = client_secret,
username = username,
password = password,
scope = scope,
} )
access_token, refresh_token = reply.access_token, reply.refresh_token
return access_token ~= nil
end
-- retrieve token using authorization code
local function retrieve_token_using_code (code, redirect_uri, scope)
scope = scope or "read_station"
local reply = HTTPS_request ("https://api.netatmo.net/oauth2/token",
{
grant_type = "authorization_code",
client_id = client_id,
client_secret = client_secret,
code = code,
redirect_uri = redirect_uri,
scope = scope,
} )
access_token, refresh_token = reply.access_token, reply.refresh_token -- , reply.expires_in
return reply.refresh_token -- need to return this to store externally
end
local function refresh_tokens (refresh)
local reply = HTTPS_request ("https://api.netatmo.net/oauth2/token",
{
grant_type = "refresh_token",
client_id = client_id,
client_secret = client_secret,
refresh_token = refresh or refresh_token,
} )
if reply.refresh_token then -- only rotate if valid, else retry next time
access_token, refresh_token = reply.access_token, reply.refresh_token
end
return reply.refresh_token -- need to return this to store externally
end
local function get_stationsdata ()
local reply = HTTPS_request ("https://api.netatmo.net/api/getstationsdata",
{
access_token = access_token
} )
return reply.body -- ALL device info!
end
-- get_measurements ()
local function get_measurements (typelist, device, module, scale)
local reply = HTTPS_request ("https://api.netatmo.net/api/getmeasure",
{
access_token = access_token,
device_id = device,
module_id = module,
type = typelist,
scale = scale or "max",
date_end = "last"
} )
if reply.body and not reply.body[1].error then
return reply.body[1].value[1] -- NB. this is a {list} of values matching the requested typelist
end
end
return { -- methods
retrieve_token_using_code = retrieve_token_using_code,
authenticate = authenticate, -- deprecated
refresh_tokens = refresh_tokens,
get_measurements = get_measurements, -- NOT NEEDED (in this plugin) WITH NEW API getstationsdata
get_stationsdata = get_stationsdata,
}
end -- netatmoAPI module
------------------------------------------------------------------------
--
-- High-level routines
--
-- key data structure for these routines is:
-- stations[stationName][moduleName] = {deviceID, moduleId, measurements} (including base device in modules)
-- build the measurements list from the device/module dashboard_data
local function build_measurements (m)
local x = {Battery= m.battery_percent} -- throw in the battery level for good measure
for name,value in pairs (m.dashboard_data or {}) do -- 2019.01.30 TODO: avoid nil pointer... but WHY?
-- remove underscores and change to CamelCase
local Name = name: gsub ("_(%w)", string.upper): gsub ("^%w", string.upper)
if type (value) ~= "table" then x[Name] = value end -- ignore table structures
end
-- add dewpoint calculation for modules with temperature and humidity
if x[T] and x[H] then
local dp = dewPoint (x[T],x[H])
x["DewPoint"] = dp - dp % 0.1
end
return x
end
-- build the stations table
local function station_data (info)
local stations = {}
local stationName = {} -- lookup table for _id --> name translation
for i,d in ipairs (info.devices) do -- go through the devices
local station_name = d.station_name
or table.concat {(d.home_name or "STATION" ), '_', i}
d.station_name = station_name -- 2020.10.15 fix missing station name
stationName[d._id] = station_name
log ("station name: " .. (d.station_name or '?'))
stations[station_name] = {[d.module_name] =
{deviceId = d._id, measurements = build_measurements (d)} } -- base device has no module _id
log ("module name: " .. (d.module_name or '?'))
for _,m in ipairs (d.modules) do -- go through the modules assigning to correct station
log ("module name: " .. (m.module_name or '?'))
stations [d.station_name][m.module_name] =
{deviceId = m.main_device, moduleId = m._id, measurements = build_measurements (m) }
end
end
return stations
end
------------------------------------------------------------------------
--
-- HTTP request handler routines: reports and plots
--
local function mapSensors (fct) -- calls fct with station / module / sensor / value information
for station, s in pairs (stationInfo) do
for module, m in pairs (s) do
for sensor, value in pairs (m.measurements) do
if type_of_metric[sensor] then -- only save interesting variables
fct (station, module, sensor, value)
end
end
end
end
end
local function formattedSensor (sensor)
local subscript = {CO2 = "CO<sub>2</sub>"}
return subscript[sensor] or sensor
end
--org chart
local function orgChart (p) -- a different visualization of the station / module / sensor structure
local d = gviz.DataTable ()
local root = "Vera / Netatmo<br><br>Last Update ".. (Timestamp or '0')
d.addColumn ("string", "Item")
d.addColumn ("string", "Parent")
d.addColumn ("string", "ToolTip")
for station, s in pairs (stationInfo) do
d.addRow {station, root, ''}
for module, m in pairs (s) do
d.addRow {module, station, ''}
local parent = module
for _,sensor in ipairs {T,H,C,P,N,R,
"SumRain1","SumRain24", W, "WindAngle", "GustStrength", "MaxWindStr"} do -- enforce specific ordering
local value = (m.measurements or {})[sensor]
if value then
local moduleSensor = module..sensor
local name = formattedSensor (sensor)
local v,u = metric.format (sensor, value)
local this = {v = moduleSensor, f = table.concat {name,"<br>",v," ",u} }
d.addRow {this, parent, ''}
parent = moduleSensor
end
end
end
end
local options = {allowHtml = true, width = p.width}
local chart = gviz.Chart "OrgChart"
return chart.draw (d, options)
end
-- metric list
local function list_Devices (p)
local d = gviz.DataTable ()
local function buildRow (station, module, sensor, value)
local _, raw_unit = metric.unit (sensor)
d.addRow {station, module, formattedSensor(sensor), value, raw_unit, metric.format (sensor, value)}
end
d.addColumn ("string", "Station")
d.addColumn ("string", "Module")
d.addColumn ("string", "Sensor")
d.addColumn ("number", "Raw Value")
d.addColumn ("string", "Raw Units")
d.addColumn ("number", "Local Value")
d.addColumn ("string", "Local Units")
mapSensors (buildRow)
local options = {allowHtml = true, height= p.height or 700, width = p.width}
local chart = gviz.Table ()
return chart.draw (d, options)
end
-- diagnostic dump of CONFIGURATION
local function diagnostics ()
local idx = {}
-- local function other (x) return "--- not number, string, or table --- "..type(x) end
local function tbl (x) return json.encode (x) end
local convert = {number = tostring, string = tostring, table = tbl}
local info = {"NETATMO CONFIGURATION PAGE at " .. os.date "%c\n"}
for a in pairs (context) do idx[#idx+1] = a end
table.sort (idx)
local b,c
for _,a in ipairs (idx) do
b = context[a]
c = (convert[type(b)] or type) (b)
info[#info+1] = table.concat {a, " = ", c or "NIL ???"}
end
return table.concat (info, '\n\n')
end
-- use authorization token provided by Netatmo Oath2 workflow
local function token (p)
local state = p.state
local code = p.code
local message
if not code or state ~= state_parameter then
message = "Error: Authorization via Netatmo website denied: " .. tostring (p.error)
else
local token = Netatmo.retrieve_token_using_code (code, redirect_uri)
if not token then
message = "Error: Failed to retrieve access token using authorization code"
else
message = "Access granted to Netatmo Weather station - now RESTART Luup engine"
set ('RefreshToken', token)
end
end
luup.log (message)
return message
end
local function authorize ()
local html = [[
<!DOCTYPE html>
<html>
<head><title>Authorize</title></head>
<body>
<p>
You will be redirected to the Netatmo website to login</br>
and authorize this plugin's access to your weather station.
</p>
<form method='get' action='https://api.netatmo.com/oauth2/authorize'>
<input type='hidden' name='client_id' value='5200dfd21977593427000024'/>
<input type='hidden' name='redirect_uri' value = ']] .. redirect_uri .. [['/>
<input type='hidden' name='scope' value='read_station'/>
<input type='hidden' name='state' value=']] .. state_parameter .. [['/>
<input type='submit' value='Login & Authorize'/> </form>
</body>
</html>
]]
return html, "text/html"
end
-- HTTP request handler
_G.HTTP_Netatmo = function (_, lul_parameters)
local dispatch = {
list = list_Devices,
authorize = authorize,
organization = orgChart,
diagnostics = diagnostics}
local function exec ()
local page = lul_parameters.page
local html = (dispatch [page] or token) (lul_parameters)
return html
end
local _, result = pcall (exec) -- catch any errors
return result
end
------------------------------------------------------------------------
--
-- Vera Luup routines
local function update_child (child, sensor, metrics)
local function setChildVariable (name)
local value = metric.format (name, metrics[name])
set (name, value, genericSID, child.deviceNo) -- use correct variable name or secondary pseudo-name
return value
end
-- each child gets the primary metric from its module, with appropriate units conversion, but NO offset
setChildVariable (sensor)
local updated = os.time()
set ("LastUpdated", updated, NetatmoSID, child.deviceNo)
-- for the primary measurement of this sensor, calibrate WITH possible user-supplied offset,
local metric_type = type_of_metric[sensor]
local Luup = LuupInfo[metric_type] -- {deviceXML = dev, service = srv, variable = var}
local val, unit = metric.format (sensor, metrics[sensor], child.offset)
set (Luup.variable, val, Luup.service, child.deviceNo) -- use correct serviceId and name
-- write a formatted line to the AltUI display variable
local line = table.concat {val, ' ', unit}
set ("DisplayLine1", line, altuiSID, child.deviceNo) -- ALTUI compatibility!!
set (Luup.variable, val, Luup.service, child.deviceNo)
-- battery level: convert into something that the UI understands
local level = metrics["Battery"]
if level then
set ("BatteryLevel", level, batterySID, child.deviceNo)
end
do -- TEMPERATURE
if (sensor == T) or (sensor == "MaxTemp") or (sensor == "MinTemp") then -- add the max/min values
local format = get (dateFormat, NetatmoSID, child.deviceNo)
local dateMax = metrics ["DateMaxTemp"]
local dateMin = metrics ["DateMinTemp"]
if sensor == "MaxTemp" then
if format ~= '' then set (dateDisplay, os.date (format, dateMax), NetatmoSID, child.deviceNo) end
else
setChildVariable "MinTemp"
setChildVariable "DateMinTemp"
set ("DateMinTemp", dateMin, Luup.service, child.deviceNo)
end
if sensor == "MinTemp" then
if format ~= '' then set (dateDisplay, os.date (format, dateMin), NetatmoSID, child.deviceNo) end
else
setChildVariable "MaxTemp"
setChildVariable "DateMaxTemp"
set ("DateMaxTemp", dateMax, Luup.service, child.deviceNo)
end
end
end
do -- HUMIDITY
if sensor == H then
-- nothing extra
end
end
do -- RAIN
if sensor == R then -- add hourly and daily cumulative values
setChildVariable "SumRain1"
local r24 = setChildVariable "SumRain24"
set ("DisplayLine2", "24hrs: " .. r24, altuiSID, child.deviceNo) -- ALTUI compatibility!!
end
end
do -- PRESSURE
if sensor == P then -- add sea-level pressure
setChildVariable "AbsolutePressure"
set ("DisplayLine2", "..." .. (metrics["PressureTrend"] or '?'), altuiSID, child.deviceNo) -- ALTUI compatibility!!
end
end
do -- CO2
if sensor == C then -- change icon with measurement thresholds
local value = metrics[sensor]
local icon
if value < 1000 then icon = "CO2_low" -- green
elseif value < 2000 then icon = "CO2_med" -- yellow
else icon = "CO2_high" -- red
end
setMetricIcon (icon, child.deviceNo)
end
end
do -- WIND
if sensor == W then
setChildVariable "MaxWindStr"
setChildVariable "DateMaxWindStr"
setChildVariable "GustStrength"
setChildVariable "GustAngle"
local v = setChildVariable "WindAngle"
setChildVariable "MaxWindAngle"
set ("DisplayLine2", (v or '?') .. '°', altuiSID, child.deviceNo) -- ALTUI compatibility!!
end
end
end
-- construct and deconstruct child device IDs
-- they are of the form: [module MAC address]-[sensor type]
local function childID ()
return {
name = function (mac, type) return mac .. '-' .. type end,
mac = function (ID) return ID:match "(.+)-%w+" end,
type = function (ID) return ID:match ".+-(%w+)" end
}
end
-- save all the measurements as Luup variables on this master plugin device
-- and any child devices for specific individual measurements (with units and calibration adjustments)
local function updateLuupVariables (stations)
local batteryWarning = 10 -- percentage threshold level to generate warning
local ID = childID ()
for _, s in pairs (stations) do
for module, m in pairs (s) do
for sensor, value in pairs (m.measurements) do
-- write metric (unchanged) to parent device
set (module..sensor, value, genericSID) -- write to master device variables (as GenericSensor1)
-- write to child device, if present
local child = ChildDevice[ID.name (m.moduleId or m.deviceId, sensor) ] -- use device ID if no module
if child then -- write to child device variables with units and offset
update_child (child, sensor, m.measurements)
end
-- Battery levels
if sensor == "Battery" and value < batteryWarning then
setNetatmoIcon ("blue", "Low Battery") -- change device icon to blue
end
end
end
end
-- parent timestamp udate
Timestamp = os.date (timeFormat) or '0'
set ('Timestamp', Timestamp ) -- say when this happened
set ('DisplayLine1', Timestamp, altuiSID) -- make it work in ALTUI too !
end
-- rotate the access keys
_G.refreshNetatmo = function ()
local delay = tokenRefresh * 60 -- normal periodic refresh of access tokens
local token = Netatmo.refresh_tokens ()
if token then
setNetatmoIcon "green"
set ('RefreshToken', token)
log 'Access tokens rotated'
else
delay = 600 -- retry in 10 minutes
setNetatmoIcon ("yellow", "Rotate fail - retrying...")
log 'Access token rotation FAILURE, retrying in 10 minutes'
end
luup.call_delay ('refreshNetatmo', delay, "") -- reschedule
return
end
-- get new measurements and update Luup variable on this device and child devices
_G.pollNetatmo = function()
luup.call_delay ('pollNetatmo', measurementPoll * 60, "") -- periodic poll of measurements
local info, status = Netatmo.get_stationsdata ()
if info then
setNetatmoIcon "green" -- although it may be blue because of low batteries...
stationInfo = station_data (info) -- create useful table with device configuration
updateLuupVariables (stationInfo) -- ...this will change it back to blue if needed
else
log (status)
setNetatmoIcon ("yellow", "Poll fail - retrying...")
end
-- local AppMemoryUsed = math.floor(collectgarbage "count") -- app's own memory usage in kB
-- set ("AppMemoryUsed", AppMemoryUsed)
log "poll complete"
collectgarbage() -- tidy up a bit
end
------------------------------------------------------------------------
--
-- Initialisation
--
-- find current family, device No, indexed by altid
local function existing_children ()
local parent = NetatmoID
local family = {}
for i,d in pairs(luup.devices) do
if d.device_num_parent == parent then
family[d.id] = i
end
end
return family
end
-- set up child devices with appropriate device files
local function create_children (stations, childSensors)
local ID = childID () -- access child ID name utilities
local makeChild = {} -- table of sensors for which to create child devices
for c in childSensors:gmatch "%w" do -- looking for individual (uppercase) letters...
local sensor = THCPNRW[c]
if sensor then makeChild[sensor] = true end
end
local family = existing_children ()
local child_devices = luup.chdev.start(NetatmoID); -- create child devices...
for _, s in pairs (stations) do
for module, m in pairs (s) do
local adopt = {} -- list for further adoption as children of an extended family
local extras = devVar (module.."Children", '', NetatmoID) -- list of adopted children
for name in extras: gmatch "%w+" do
if type_of_metric[name] then adopt[name] = true end -- ...only if we know the type of metric
end
for sensor in pairs (m.measurements) do
local child = ID.name (m.moduleId or m.deviceId, sensor)
family[child] = nil -- remove from current list
if makeChild[sensor] or adopt[sensor] then -- only create or adopt children for required sensor types
luup.chdev.append(
NetatmoID, -- parent (this device)
child_devices, -- pointer from above "start" call
child, -- child ID
module..' - '..sensor, -- child device description
"", -- serviceId defined in device file
LuupInfo[type_of_metric[sensor]].deviceXML, -- device file
"", -- no implementation file required
"", -- no parameters to set
false) -- not embedded child devices (can go in any room)
end
end
end
end
-- 2020.05.11 create 'missing' devices
for child, devNo in pairs(family) do
local missing = "device '[%d]%s' not found in current list of modules"
local d = luup.devices[devNo]
local dtype = d.device_type
dtype = (dtype:match "Temperature" and T) or (dtype:match "Humidity" and T) or 'X'
log (missing: format (devNo, d.description))
luup.chdev.append(
NetatmoID, -- parent (this device)
child_devices, -- pointer from above "start" call
child, -- child ID
d.description, -- child device description
"", -- serviceId defined in device file
LuupInfo[dtype].deviceXML, -- device file
"", -- no implementation file required
"", -- no parameters to set
false) -- not embedded child devices (can go in any room)
end
luup.chdev.sync(NetatmoID, child_devices) -- any changes in configuration will cause a restart at this point
for deviceNo, d in pairs (luup.devices) do -- pick up the child device numbers from their IDs
if d.device_num_parent == NetatmoID then
local sensor = ID.type (d.id)
local unit = metric.unit (sensor) or 'unknown'
log ('Child = '..deviceNo.. ' '..d.id..' ('..unit.. ')' )
-- calibration offset
local offset = devVar (calibrationOffset, "0", deviceNo) -- default is no offset
-- units
set (unitsVariable, unit, NetatmoSID, deviceNo)
-- icon
setMetricIcon (sensor, deviceNo)
ChildDevice [d.id] = {deviceNo = deviceNo, offset = tonumber (offset) or 0}
end
end
return ChildDevice
end
--@reneboer, http://forum.micasaverde.com/index.php/topic,16276.msg203653.html#msg203653
-- On UI5 it have a true or false as parameter, the 0,1,2 should be UI7 specific.
local function set_failure (status)
if (luup.version_major < 7) then status = status ~= 0 end -- fix UI5 status type
luup.set_failure(status)
end
local function clean_up_old_variables ()
local unwanted = "ClientID ClientSecret Username Password TokenRefresh AppMemoryUsed"
if get "Username" then
for name in unwanted: gmatch "%a+" do
set (name)
end
end
end
-- init () called on startup
function init (lul_device)
NetatmoID = lul_device -- save the global device ID