1if not modules then modules = { } end modules ['util-evo'] = {
2 version = 1.002,
3 comment = "library for fetching data from an evohome device",
4 author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5 copyright = "PRAGMA ADE",
6 license = "see context related readme files"
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
39require("util-jsn")
40
41local next, type, setmetatable, rawset, rawget = next, type, setmetatable, rawset, rawget
42local json = utilities.json
43local formatters = string.formatters
44local floor, div = math.floor, math.div
45local resultof, ostime, osdate, ossleep = os.resultof, os.time, os.date, os.sleep
46local jsontolua, jsontostring = json.tolua, json.tostring
47local savetable, loadtable, sortedkeys, sortedhash = table.save, table.load, table.sortedkeys, table.sortedhash
48local setmetatableindex, setmetatablenewindex = table.setmetatableindex, table.setmetatablenewindex
49local replacer = utilities.templates.replacer
50local lower = string.lower
51
52local applicationid = "b013aa26-9724-4dbd-8897-048b9aada249"
53
54
55local report = logs.reporter("evohome")
56local trace = false
57
58trackers.register("evohome.trace",function(v) trace = v end)
59
60local defaultpresets = {
61 interval = 30 * 60,
62 files = {
63 everything = "evohome-everything.lua",
64 history = "evohome-history.lua",
65 latest = "evohome-latest.lua",
66 schedules = "evohome-schedules.lua",
67 actions = "evohome-actions.lua",
68 template = "evohome.lmx",
69 },
70 credentials = {
71
72
73
74
75 },
76}
77
78local validzonetypes = {
79 ZoneTemperatureControl = true,
80 RadiatorZone = true,
81 ZoneValves = true,
82}
83
84local function validfile(presets,filename)
85 if lfs.isfile(filename) then
86
87 return filename
88 end
89 if file.pathpart(filename) ~= "" then
90
91 return filename
92 end
93 local presetsname = presets.filename
94 if not presetsname then
95
96 return filename
97 end
98
99 return file.join(file.pathpart(presetsname),filename)
100end
101
102local function validpresets(presets)
103 if type(presets) ~= "table" then
104 report("invalid presets, no table")
105 return
106 end
107 local credentials = presets.credentials
108 if not credentials then
109 report("invalid presets, no credentials")
110 return
111 end
112 local gateways = presets.gateways
113 if not gateways then
114 report("invalid presets, no gateways")
115 return
116 end
117 local files = presets.files
118 if not files then
119 report("invalid presets, no files")
120 return
121 end
122 for k, v in next, files do
123 files[k] = validfile(presets,v) or v
124 end
125 local data = presets.data
126 if not data then
127 data = { }
128 presets.data = data
129 end
130 local g = data.gateways
131 if not g then
132 local g = { }
133 data.gateways = g
134 for i=1,#gateways do
135 local gi = gateways[i]
136 g[gi.macaddress] = gi
137 end
138 end
139 local zones = data.zones
140 if not zones then
141 zones = { }
142 data.zones = zones
143 setmetatablenewindex(zones,function(t,k,v) rawset(t,lower(k),v) end)
144 setmetatableindex (zones,function(t,k) return rawget(t,lower(k)) end)
145 end
146 local states = data.states
147 if not states then
148 states = { }
149 data.states = states
150 setmetatablenewindex(states,function(t,k,v) rawset(t,lower(k),v) end)
151 setmetatableindex (states,function(t,k) return rawget(t,lower(k)) end)
152 end
153 setmetatableindex(presets,defaultpresets)
154 setmetatableindex(credentials,defaultpresets.credentials)
155 setmetatableindex(files,defaultpresets.files)
156 return presets
157end
158
159local function loadedtable(filename)
160 if type(filename) == "string" then
161 for i=1,10 do
162 local t = loadtable(filename)
163 if t then
164 report("file %a loaded",filename)
165 return t
166 else
167 ossleep(1/4)
168 end
169 end
170 end
171 report("file %a not loaded",filename)
172 return { }
173end
174
175local function savedtable(filename,data,trace)
176 savetable(filename,data)
177 if trace then
178 report("file %a saved",filename)
179 end
180end
181
182local function loadpresets(filename)
183 local presets = loadtable(filename)
184 if presets then
185 presets.filename = filename
186 presets.filepath = file.expandname(file.pathpart(filename))
187
188 end
189 return presets
190end
191
192local function loadhistory(filename)
193 if type(filename) == "table" and validpresets(filename) then
194 filename = filename.files and filename.files.history
195 end
196 return loadedtable(filename)
197end
198
199local function loadeverything(filename)
200 if type(filename) == "table" and validpresets(filename) then
201 filename = filename.files and filename.files.everything
202 end
203 return loadedtable(filename)
204end
205
206local function loadlatest(filename)
207 if type(filename) == "table" and validpresets(filename) then
208 filename = filename.files and filename.files.latest
209 end
210 return loadedtable(filename)
211end
212
213local function result(t,fmt,a,b,c)
214 if t then
215 report(fmt,a or "done",b or "done",c or "done","done")
216 return t
217 else
218 report(fmt,a or "failed",b or "failed",c or "failed","failed")
219 end
220end
221
222local f = replacer (
223 [[curl ]] ..
224 [[--silent --insecure ]] ..
225 [[-X POST ]] ..
226 [[-H "Authorization: Basic YjAxM2FhMjYtOTcyNC00ZGJkLTg4OTctMDQ4YjlhYWRhMjQ5OnRlc3Q=" ]] ..
227 [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
228 [[-d "Content-Type=application/x-www-form-urlencoded; charset=utf-8" ]] ..
229 [[-d "Host=rs.alarmnet.com/" ]] ..
230 [[-d "Cache-Control=no-store no-cache" ]] ..
231 [[-d "Pragma=no-cache" ]] ..
232 [[-d "grant_type=password" ]] ..
233 [[-d "scope=EMEA-V1-Basic EMEA-V1-Anonymous EMEA-V1-Get-Current-User-Account" ]] ..
234 [[-d "Username=%username%" ]] ..
235 [[-d "Password=%password%" ]] ..
236 [[-d "Connection=Keep-Alive" ]] ..
237 [["https://tccna.honeywell.com/Auth/OAuth/Token"]]
238)
239
240local function getaccesstoken(presets)
241 if validpresets(presets) then
242 local c = presets.credentials
243 local s = c and f {
244 username = c.username,
245 password = c.password,
246 applicationid = applicationid,
247 }
248 local r = s and resultof(s)
249 local t = r and jsontolua(r)
250 return result(t,"getting access token %a")
251 end
252 return result(false,"getting access token %a")
253end
254
255local f = replacer (
256 [[curl ]] ..
257 [[--silent --insecure ]] ..
258 [[-H "Authorization: bearer %accesstoken%" ]] ..
259 [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
260 [[-H "applicationId: %applicationid%" ]] ..
261 [["https://tccna.honeywell.com/WebAPI/emea/api/v1/userAccount"]]
262)
263
264local function getuserinfo(presets)
265 if validpresets(presets) then
266 local c = presets.credentials
267 local s = c and f {
268 accesstoken = c.accesstoken,
269 applicationid = c.applicationid,
270 }
271 local r = s and resultof(s)
272 local t = r and jsontolua(r)
273 return result(t,"getting user info for %a")
274 end
275 return result(false,"getting user info for %a")
276end
277
278local f = replacer (
279 [[curl ]] ..
280 [[--silent --insecure ]] ..
281 [[-H "Authorization: bearer %accesstoken%" ]] ..
282 [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
283 [[-H "applicationId: %applicationid%" ]] ..
284 [["https://tccna.honeywell.com/WebAPI/emea/api/v1/location/installationInfo?userId=%userid%&includeTemperatureControlSystems=True"]]
285)
286
287local function getlocationinfo(presets)
288 if validpresets(presets) then
289 local c = presets.credentials
290 local s = c and f {
291 accesstoken = c.accesstoken,
292 applicationid = applicationid,
293 userid = c.userid,
294 }
295 local r = s and resultof(s)
296 local t = r and jsontolua(r)
297 return result(t,"getting location info for %a")
298 end
299 return result(false,"getting location info for %a")
300end
301
302local f = replacer (
303 [[curl ]] ..
304 [[--silent --insecure ]] ..
305 [[-H "Authorization: bearer %accesstoken%" ]] ..
306 [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
307 [[-H "applicationId: %applicationid%" ]] ..
308 [["https://tccna.honeywell.com/WebAPI/emea/api/v1/temperatureZone/%zoneid%/schedule"]]
309)
310
311local function getschedule(presets,zonename)
312 if validpresets(presets) then
313 local zoneid = presets.data.zones[zonename].zoneId
314 if zoneid then
315 local c = presets.credentials
316 local s = c and f {
317 accesstoken = c.accesstoken,
318 applicationid = applicationid,
319 zoneid = zoneid,
320 }
321 local r = s and resultof(s)
322 local t = r and jsontolua(r)
323 return result(t,"getting schedule for zone %a, %s",zonename or "?")
324 end
325 end
326 return result(false,"getting schedule for zone %a, %s",zonename or "?")
327end
328
329local f = replacer (
330 [[curl ]] ..
331 [[--silent --insecure ]] ..
332 [[-H "Authorization: bearer %accesstoken%" ]] ..
333 [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
334 [[-H "applicationId: %applicationid%" ]] ..
335 [["https://tccna.honeywell.com/WebAPI/emea/api/v1/location/%locationid%/status?includeTemperatureControlSystems=True" ]]
336)
337
338local function getstatus(presets,locationid,locationname)
339 if locationid and validpresets(presets) then
340 local c = presets.credentials
341 local s = c and f {
342 accesstoken = c.accesstoken,
343 applicationid = applicationid,
344 locationid = locationid,
345 }
346 local r = s and resultof(s)
347 local t = r and jsontolua(r)
348 return result(t and t.gateways and t,"getting status for location %a, %s",locationname or "?")
349 end
350 return result(false,"getting status for location %a, %s",locationname or "?")
351end
352
353local function validated(presets)
354 if validpresets(presets) then
355 local data = getlocationinfo(presets)
356 if data and type(data) == "table" and data[1] and data[1].locationInfo then
357 return true
358 else
359 local data = getaccesstoken(presets)
360 if data then
361 presets.credentials.accesstoken = data.access_token
362 local data = getuserinfo(presets)
363 if data then
364 presets.credentials.userid = data.userId
365 return true
366 end
367 end
368 end
369 end
370end
371
372local function findzone(presets,name)
373 if not presets then
374 return
375 end
376 local data = presets.data
377 if not data then
378 return
379 end
380 local usedzones = data.zones
381 return usedzones and usedzones[name]
382end
383
384local function getzonenames(presets)
385 if not presets then
386 return { }
387 end
388 local data = presets.data
389 if not data then
390 return { }
391 end
392 local t = sortedkeys(data.zones or { })
393 for i=1,#t do
394 t[i] = lower(t[i])
395 end
396 return t
397end
398
399local function gettargets(zone)
400 local schedule = zone.schedule
401 local min = false
402 local max = false
403 if schedule then
404 local schedules = schedule.dailySchedules
405 if schedules then
406 for i=1,#schedules do
407 local switchpoints = schedules[i].switchpoints
408 for i=1,#switchpoints do
409 local m = switchpoints[i].temperature
410 if not min or m < min then
411 min = m
412 end
413 if not max or m > max then
414 max = m
415 end
416 end
417 end
418 else
419 report("zone %a has no schedule",name)
420 end
421 end
422 return min, max
423end
424
425local function updatezone(presets,name,zone)
426 if not zone then
427 zone = findzone(presets,name)
428 end
429 if zone then
430 local oldtarget = presets.data.states[name]
431 local min = zone.heatSetpointCapabilities.minHeatSetpoint or 5
432 local max = zone.heatSetpointCapabilities.maxHeatSetpoint or 12
433 local mintarget, maxtarget = gettargets(zone)
434
435 if mintarget == false then
436 if min < 5 then
437 mintarget = 5
438
439 else
440 mintarget = min
441 end
442 end
443 if maxtarget == false then
444 if max > 18.5 then
445 maxtarget = 18.5
446
447 else
448 maxtarget = max
449 end
450 end
451 local current = zone.temperatureStatus.temperature or 0
452 local target = zone.heatSetpointStatus.targetTemperature
453 local mode = zone.heatSetpointStatus.setpointMode
454 local state = (mode == "FollowSchedule" and "schedule" ) or
455 (mode == "PermanentOverride" and target <= mintarget and "permanent") or
456 (mode == "TemporaryOverride" and target <= mintarget and "off" ) or
457 (mode == "TemporaryOverride" and target >= maxtarget and "on" ) or
458 ( "unknown" )
459 local t = {
460 name = zone.name,
461 id = zone.zoneId,
462 schedule = zone.schedule,
463 mode = mode,
464 current = current,
465 target = target,
466 min = min,
467 max = max,
468 state = state,
469 lowest = mintarget,
470 highest = maxtarget,
471 }
472
473 presets.data.states[name] = t
474 return t
475 end
476end
477
478
479local function geteverything(presets,noschedules)
480 if validated(presets) then
481 local data = getlocationinfo(presets)
482 if data then
483 local usedgateways = presets.data.gateways
484 local usedzones = presets.data.zones
485 for i=1,#data do
486 local gateways = data[i].gateways
487 local locationinfo = data[i].locationInfo
488 local locationid = locationinfo and locationinfo.locationId
489 if gateways and locationid then
490 local status = getstatus(presets,locationid,locationinfo.name)
491 if status then
492 for i=1,#gateways do
493 local gatewaystatus = status.gateways[i]
494 local gatewayinfo = gateways[i]
495 local gatewaysystems = gatewayinfo.temperatureControlSystems
496 local info = gatewayinfo.gatewayInfo
497 local statussystems = gatewaystatus.temperatureControlSystems
498 if gatewaysystems and statussystems and info then
499 local mac = info.mac
500 if usedgateways[mac] then
501 report("%s gateway with mac address %a","using",mac)
502 for j=1,#gatewaysystems do
503 local gatewayzones = gatewaysystems[j].zones
504 local zonestatus = statussystems[j].zones
505 if gatewayzones and zonestatus then
506 for k=1,#gatewayzones do
507 local zonestatus = zonestatus[k]
508 local gatewayzone = gatewayzones[k]
509 if zonestatus and gatewayzone then
510 local zonename = zonestatus.name
511 local zoneid = zonestatus.zoneId
512 if validzonetypes[gatewayzone.zoneType] and zonename == gatewayzone.name then
513 gatewayzone.heatSetpointStatus = zonestatus.heatSetpointStatus
514 gatewayzone.temperatureStatus = zonestatus.temperatureStatus
515 local zonestatus = usedzones[zonename]
516 local schedule = zonestatus and zonestatus.schedule
517 usedzones[zonename] = gatewayzone
518 if schedule and noschedules then
519 gatewayzone.schedule = schedule
520 else
521 gatewayzone.schedule = getschedule(presets,zonename)
522 end
523 updatezone(presets,zonename,gatewayzone)
524 end
525 end
526 end
527 end
528 end
529 else
530 report("%s gateway with mac address %a","skipping",mac)
531 end
532 end
533 end
534 end
535 end
536 end
537 savedtable(presets.files.everything,data)
538 return result(data,"getting everything, %s")
539 end
540 end
541 return result(false,"getting everything, %s")
542end
543
544local function gettemperatures(presets)
545 if validated(presets) then
546 local data = loadeverything(presets)
547 if not data or not next(data) then
548 data = geteverything(presets)
549 end
550 if data then
551 local updated = false
552 for i=1,#data do
553 local gateways = data[i].gateways
554 local locationinfo = data[i].locationInfo
555 if locationinfo then
556 local locationid = locationinfo.locationId
557 if gateways then
558 local status = getstatus(presets,locationid,locationinfo.name)
559 if status then
560 for i=1,#gateways do
561 local g = status.gateways[i]
562 local gateway = gateways[i]
563 local systems = gateway.temperatureControlSystems
564 if systems then
565 local s = g.temperatureControlSystems
566 for i=1,#systems do
567 local zones = systems[i].zones
568 if zones then
569 local z = s[i].zones
570 for i=1,#zones do
571 local zone = zones[i]
572 if validzonetypes[zone.zoneType] then
573 local z = z[i]
574 if z.name == zone.name then
575 zone.temperatureStatus = z.temperatureStatus
576 updated = true
577 end
578 end
579 end
580 end
581 end
582 end
583 end
584 end
585 else
586 report("no gateways")
587 end
588 else
589 report("no location info")
590 end
591 end
592 if updated then
593 data.time = ostime()
594 savedtable(presets.files.latest,data)
595 end
596 return result(data,"getting temperatures, %s")
597 end
598 end
599 return result(false,"getting temperatures, %s")
600end
601
602local function setmoment(target,time,data)
603 if not time then
604 time = ostime()
605 end
606 local t = osdate("*t",time )
607 local c_year, c_month, c_day, c_hour, c_minute = t.year, t.month, t.day, t.hour, t.min
608
609 local years = target.years if not years then years = { } target.years = years end
610 local d_year = years[c_year] if not d_year then d_year = { } years[c_year] = d_year end
611 local months = d_year.months if not months then months = { } d_year.months = months end
612 local d_month = months[c_month] if not d_month then d_month = { } months[c_month] = d_month end
613 local days = d_month.days if not days then days = { } d_month.days = days end
614 local d_day = days[c_day] if not d_day then d_day = { } days[c_day] = d_day end
615 local hours = d_day.hours if not hours then hours = { } d_day.hours = hours end
616 local d_hour = hours[c_hour] if not d_hour then d_hour = { } hours[c_hour] = d_hour end
617
618 c_minute = div(c_minute,15) + 1
619
620 local d_last = d_hour[c_minute]
621 if d_last then
622 for k, v in next, data do
623 local d = d_last[k]
624 if d then
625 data[k] = (d + v) / 2
626 end
627 end
628 end
629 d_hour[c_minute] = data
630
631 target.lasttime = {
632 year = c_year,
633 month = c_month,
634 day = c_day,
635 hour = c_hour,
636 minute = c_minute,
637 }
638end
639
640local function loadtemperatures(presets)
641 if validpresets(presets) then
642 local status = loadlatest(presets)
643 if not status or not next(status) then
644 status = loadeverything(presets)
645 end
646 if status then
647 local usedgateways = presets.data.gateways
648 for i=1,#status do
649 local gateways = status[i].gateways
650 if gateways then
651 for i=1,#gateways do
652 local gatewayinfo = gateways[i]
653 local systems = gatewayinfo.temperatureControlSystems
654 local info = gatewayinfo.gatewayInfo
655 if systems and info and usedgateways[info.mac] then
656 for i=1,#systems do
657 local zones = systems[i].zones
658 if zones then
659 local summary = { time = status.time }
660 for i=1,#zones do
661 local zone = zones[i]
662 if validzonetypes[zone.zoneType] then
663 summary[#summary+1] = updatezone(presets,zone.name,zone)
664 end
665 end
666 return result(summary,"loading temperatures, %s")
667 end
668 end
669 end
670 end
671 end
672 end
673 end
674 end
675 return result(false,"loading temperatures, %s")
676end
677
678local function updatetemperatures(presets)
679 if validpresets(presets) then
680 local everythingname = presets.files.everything
681 local latestname = presets.files.latest
682 local historyname = presets.files.history
683 if (everythingname or latestname) and historyname then
684 gettemperatures(presets)
685 local t = loadtemperatures(presets)
686 if t then
687 local data = { }
688 for i=1,#t do
689 local ti = t[i]
690 data[ti.name] = ti.current
691 end
692 local history = loadhistory(historyname) or { }
693 setmoment(history,ostime(),data)
694 savedtable(historyname,history)
695 return result(t,"updating temperatures, %s")
696 end
697 end
698 end
699 return result(false,"updating temperatures, %s")
700end
701
702local function getzonestate(presets,name)
703 return validpresets(presets) and presets.data.states[name]
704end
705
706local f = replacer (
707 [[curl ]] ..
708 [[--silent --insecure ]] ..
709 [[-X PUT ]] ..
710 [[-H "Authorization: bearer %accesstoken%" ]] ..
711 [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
712 [[-H "applicationId: %applicationid%" ]] ..
713 [[-H "Content-Type: application/json" ]] ..
714 [[-d "%[settings]%" ]] ..
715 [["https://tccna.honeywell.com/WebAPI/emea/api/v1/temperatureZone/%zoneid%/heatSetpoint"]]
716)
717
718local function untilmidnight()
719 local t = osdate("*t")
720 t.hour = 23
721 t.min = 59
722 t.sec = 59
723 return osdate("%Y-%m-%dT%H:%M:%SZ",ostime(t))
724end
725
726local followschedule = {
727
728 SetpointMode = "FollowSchedule",
729}
730
731local function setzonestate(presets,name,temperature,permanent)
732 local zone = findzone(presets,name)
733 if zone then
734 local m = followschedule
735 if type(temperature) == "number" and temperature > 0 then
736 if permanent then
737 m = {
738 HeatSetpointValue = temperature,
739 SetpointMode = "PermanentOverride",
740 }
741 else
742 m = {
743 HeatSetpointValue = temperature,
744 SetpointMode = "TemporaryOverride",
745 TimeUntil = untilmidnight(),
746 }
747 end
748 end
749 local s = f {
750 accesstoken = presets.credentials.accesstoken,
751 applicationid = applicationid,
752 zoneid = zone.zoneId,
753 settings = jsontostring(m),
754 }
755 local r = s and resultof(s)
756 local t = r and jsontolua(r)
757
758
759 return result(t,"setting state of zone %a, %s",name)
760 end
761 return result(false,"setting state of zone %a, %s",name)
762end
763
764local function resetzonestate(presets,name)
765 setzonestate(presets,name)
766end
767
768
769
770local function update(presets,noschedules)
771 local everything = geteverything(presets,noschedules)
772 if everything then
773 presets.data.everything = everything
774 return presets
775 end
776end
777
778local function initialize(filename)
779 local presets = loadpresets(filename)
780 if presets then
781 return update(presets)
782 end
783end
784
785local function off(presets,name)
786 local zone = presets and getzonestate(presets,name)
787 if zone then
788 setzonestate(presets,name,zone.lowest)
789 end
790end
791
792local function on(presets,name,temperature)
793 local zone = presets and getzonestate(presets,name)
794 if zone then
795 setzonestate(presets,name,temperature or zone.highest)
796 end
797end
798
799local function schedule(presets,name)
800 local zone = presets and getzonestate(presets,name)
801 if zone then
802 resetzonestate(presets,name)
803 end
804end
805
806local function permanent(presets,name)
807 local zone = presets and getzonestate(presets,name)
808 if zone then
809 setzonestate(presets,name,zone.lowest,true)
810 end
811end
812
813
814
815local function settask(presets,when,tag,action)
816 if when == "tomorrow" then
817 local list = presets.scheduled
818 if not list then
819 list = loadtable(presets.files.schedules) or { }
820 presets.scheduled = list
821 end
822 if action then
823 list[tag] = {
824 time = ostime() + 24*60*60,
825 done = false,
826 category = category,
827 action = action,
828 tag = tag,
829 }
830 else
831 list[tag] = nil
832 end
833 savedtable(presets.files.schedules,list,false)
834 end
835end
836
837local function gettask(presets,when,tag)
838 if when == "tomorrow" then
839 local list = presets.scheduled
840 if not list then
841 list = loadtable(presets.files.schedules) or { }
842 presets.scheduled = list
843 end
844 return list[tag]
845 end
846end
847
848local function resettask(presets,when,tag)
849 settask(presets,when,tag)
850end
851
852local function checktasks(presets)
853 local list = presets.scheduled
854 if not list then
855 list = loadtable(presets.files.schedules) or { }
856 presets.scheduled = list
857 end
858 if list then
859 local t = osdate("*t")
860 local q = { }
861 for k, v in next, list do
862 local d = osdate("*t",v.time)
863 if not v.done and d.year == t.year and d.month == t.month and d.day == t.day then
864 local a = v.action
865 if type(a) == "function" then
866 a()
867 end
868 v.done = true
869 end
870 if d.year <= t.year and d.month <= t.month and d.day < t.day then
871 q[k] = true
872 end
873 end
874 if next(q) then
875 for k, v in next, q do
876 list[q] = nil
877 end
878 savedtable(presets.files.schedules,list)
879 end
880 return list
881 end
882end
883
884
885
886local function settomorrow(presets,tag,action)
887 settask(presets,"tomorrow",tag,action)
888end
889
890local function resettomorrow(presets,tag)
891 settask(presets,"tomorrow",tag)
892end
893
894local function tomorrowset(presets,tag)
895 return gettask(presets,"tomorrow",tag) and true or false
896end
897
898
899
900local evohome
901
902local function poller(presets)
903
904 if type(presets) ~= "string" then
905 report("invalid presets file")
906 os.exit()
907 end
908 report("loading presets from %a",presets)
909 local presets = loadpresets(presets)
910 if not validpresets(presets) then
911 report("invalid presets, aborting")
912 os.exit()
913 end
914
915 local actions = presets.files.actions
916 if type(actions) ~= "string" then
917 report("invalid actions file")
918 os.exit()
919 end
920 report("loading actions from %a",actions)
921 local actions = loadtable(actions)
922 if type(actions) ~= "table" then
923 report("invalid actions, aborting")
924 os.exit()
925 end
926 actions = actions.actions
927 if type(actions) ~= "table" then
928 report("invalid actions file, no actions subtable")
929 os.exit()
930 end
931
932 report("updating device status")
933 update(presets)
934
935 presets.report = report
936 presets.evohome = evohome
937 presets.results = { }
938
939 function presets.getstate(name)
940 return getzonestate(presets,name)
941 end
942 function presets.tomorrowset(name)
943 return tomorrowset(presets,name)
944 end
945
946 local template = actions.template or presets.files.template
947
948 local process = function(t)
949 local category = t.category
950 local action = t.action
951 if category and action then
952 local c = actions[category]
953 if c then
954 local a = c[action]
955 if type(a) == "function" then
956 report("category %a, action %a, executing",category,action)
957 presets.results.template = template
958 a(presets)
959 update(presets,true)
960 else
961 report("category %a, action %a, invalid action, known: %, t",category,action,sortedkeys(c))
962 end
963 else
964 report("category %a, action %a, invalid category, known categories: %, t",category,action,sortedkeys(actions))
965 end
966 else
967
968 end
969 end
970
971 local delay = presets.delay or 10
972 local interval = 15 * 60
973 local interval = 60 * 60
974 local refresh = 5 * 60
975 local passed = 0
976 local step = function()
977 if passed > interval then
978 report("refreshing states, every %i seconds",interval)
979
980
981
982 update(presets)
983 passed = 0
984 else
985 passed = passed + delay
986 end
987 checktasks(presets)
988 return delay
989 end
990
991 presets.refreshtime = refresh
992
993 return step, process, presets
994end
995
996local function alloff(presets)
997 local zones = getzonenames(presets)
998 if zones then
999 for i=1,#zones do
1000 setzonestate(presets,zones[i],5,true)
1001 end
1002 end
1003end
1004
1005
1006
1007evohome = {
1008 helpers = {
1009 getaccesstoken = getaccesstoken,
1010 getuserinfo = getuserinfo,
1011 getlocationinfo = getlocationinfo,
1012 getschedule = getschedule,
1013
1014 geteverything = geteverything,
1015 gettemperatures = gettemperatures,
1016 getzonestate = getzonestate,
1017 setzonestate = setzonestate,
1018 resetzonestate = resetzonestate,
1019 getzonedata = findzone,
1020 getzonenames = getzonenames,
1021
1022 loadpresets = loadpresets,
1023 loadhistory = loadhistory,
1024 loadeverything = loadeverything,
1025 loadtemperatures = loadtemperatures,
1026
1027 updatetemperatures = updatetemperatures,
1028 },
1029 actions= {
1030 initialize = initialize,
1031 update = update,
1032
1033 off = off,
1034 on = on,
1035 schedule = schedule,
1036 permanent = permanent,
1037
1038 alloff = alloff,
1039
1040 settomorrow = settomorrow,
1041 resettomorrow = resettomorrow,
1042 tomorrowset = tomorrowset,
1043
1044 poller = poller,
1045 }
1046}
1047
1048if utilities then
1049 utilities.evohome = evohome
1050end
1051
1052
1053
1054
1055
1056return evohome
1057
1058 |