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