summaryrefslogtreecommitdiff
path: root/tex/context/base/mkiv/util-evo.lua
diff options
context:
space:
mode:
Diffstat (limited to 'tex/context/base/mkiv/util-evo.lua')
-rw-r--r--tex/context/base/mkiv/util-evo.lua1002
1 files changed, 1002 insertions, 0 deletions
diff --git a/tex/context/base/mkiv/util-evo.lua b/tex/context/base/mkiv/util-evo.lua
new file mode 100644
index 000000000..7f0b59ac4
--- /dev/null
+++ b/tex/context/base/mkiv/util-evo.lua
@@ -0,0 +1,1002 @@
+if not modules then modules = { } end modules ['util-evo'] = {
+ version = 1.002,
+ comment = "library for fetching data from an evohome device",
+ author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+ copyright = "PRAGMA ADE",
+ license = "see context related readme files"
+}
+
+-- When I needed a new boiler for heating I decided to replace a partial
+-- (experimental) zwave few-zone solution by the honeywell evohome system that can
+-- drive opentherm. I admit that I was not that satified beforehand with the fact
+-- that one has to go via some outside portal to communicate with the box but lets
+-- hope that this will change (I will experiment with the additional usb interface
+-- later). Anyway, apart from integrating it into my home automation setup so that I
+-- can add control based on someone present in a zone, I wanted to be able to render
+-- statistics. So that's why we have a module in ConTeXt for doing that. It's also
+-- an example of Lua and abusing LuaTeX for something not related to typesetting.
+--
+-- As with other scripts, it assumes that mtxrun is used so that we have the usual
+-- Lua libraries present.
+--
+-- The code is not that complex but figuring out the right request takes bit of
+-- searching the web. There is an api specification at:
+--
+-- https://developer.honeywell.com/api-methods?field_smart_method_tags_tid=All
+--
+-- Details like the application id can be found in several places. There are snippets
+-- of (often partial or old) code on the web but still one needs to experiment and
+-- combine information. We assume unique zone names and ids across gateways; I only
+-- have one installed anyway.
+--
+-- The original application was to just get the right information for generating
+-- statistics but in the meantime I also use this code to add additional functionality
+-- to the system, for instance switching between rooms (office, living room, attic) and
+-- absence for one or more rooms.
+
+-- todo: %path% in filenames
+
+require("util-jsn")
+
+local next, type, setmetatable, rawset, rawget = next, type, setmetatable, rawset, rawget
+local json = utilities.json
+local formatters = string.formatters
+local floor, div = math.floor, math.div
+local resultof, ostime, osdate, ossleep = os.resultof, os.time, os.date, os.sleep
+local jsontolua, jsontostring = json.tolua, json.tostring
+local savetable, loadtable, sortedkeys = table.save, table.load, table.sortedkeys
+local setmetatableindex, setmetatablenewindex = table.setmetatableindex, table.setmetatablenewindex
+local replacer = utilities.templates.replacer
+local lower = string.lower -- no utf support yet (encoding needs checking in evohome)
+
+local applicationid = "b013aa26-9724-4dbd-8897-048b9aada249"
+----- applicationid = "91db1612-73fd-4500-91b2-e63b069b185c"
+
+local report = logs.reporter("evohome")
+local trace = false
+
+trackers.register("evohome.trace",function(v) trace = v end) -- not yet used
+
+local defaultpresets = {
+ interval = 30 * 60,
+ files = {
+ everything = "evohome-everything.lua",
+ history = "evohome-history.lua",
+ latest = "evohome-latest.lua",
+ schedules = "evohome-schedules.lua",
+ actions = "evohome-actions.lua",
+ template = "evohome.lmx",
+ },
+ credentials = {
+ -- username = "unset",
+ -- password = "unset",
+ -- accesstoken = "unset",
+ -- userid = "unset",
+ },
+}
+
+local validzonetypes = {
+ ZoneTemperatureControl = true,
+ RadiatorZone = true,
+ ZoneValves = true,
+}
+
+local function validfile(presets,filename)
+ if lfs.isfile(filename) then
+ -- we're okay
+ return filename
+ end
+ if file.pathpart(filename) ~= "" then
+ -- can be a file that has to be created
+ return filename
+ end
+ local presetsname = presets.filename
+ if not presetsname then
+ -- hope for the best
+ return filename
+ end
+ -- we now have the full path
+ return file.join(file.pathpart(presetsname),filename)
+end
+
+local function validpresets(presets)
+ if type(presets) ~= "table" then
+ report("invalid presets, no table")
+ return
+ end
+ local credentials = presets.credentials
+ if not credentials then
+ report("invalid presets, no credentials")
+ return
+ end
+ local gateways = presets.gateways
+ if not gateways then
+ report("invalid presets, no gateways")
+ return
+ end
+ local files = presets.files
+ if not files then
+ report("invalid presets, no files")
+ return
+ end
+ for k, v in next, files do
+ files[k] = validfile(presets,v) or v
+ end
+ local data = presets.data
+ if not data then
+ data = { }
+ presets.data = data
+ end
+ local g = data.gateways
+ if not g then
+ local g = { }
+ data.gateways = g
+ for i=1,#gateways do
+ local gi = gateways[i]
+ g[gi.macaddress] = gi
+ end
+ end
+ local zones = data.zones
+ if not zones then
+ zones = { }
+ data.zones = zones
+ setmetatablenewindex(zones,function(t,k,v) rawset(t,lower(k),v) end)
+ setmetatableindex (zones,function(t,k) return rawget(t,lower(k)) end)
+ end
+ local states = data.states
+ if not states then
+ states = { }
+ data.states = states
+ setmetatablenewindex(states,function(t,k,v) rawset(t,lower(k),v) end)
+ setmetatableindex (states,function(t,k) return rawget(t,lower(k)) end)
+ end
+ setmetatableindex(presets,defaultpresets)
+ setmetatableindex(credentials,defaultpresets.credentials)
+ setmetatableindex(files,defaultpresets.files)
+ return presets
+end
+
+local function loadedtable(filename)
+ if type(filename) == "string" then
+ for i=1,10 do
+ local t = loadtable(filename)
+ if t then
+ return t
+ else
+ ossleep(1/4)
+ end
+ end
+ end
+ return { }
+end
+
+local function loadpresets(filename)
+ local presets = loadtable(filename)
+ if presets then
+ presets.filename = filename
+ presets.filepath = file.expandname(file.pathpart(filename))
+ -- package.extraluapath(presets.filepath) -- better do that elsewhere and once
+ end
+ return presets
+end
+
+local function loadhistory(filename)
+ if type(filename) == "table" and validpresets(filename) then
+ filename = filename.files and filename.files.history
+ end
+ return loadedtable(filename)
+end
+
+local function loadeverything(filename)
+ if type(filename) == "table" and validpresets(filename) then
+ filename = filename.files and filename.files.everything
+ end
+ return loadedtable(filename)
+end
+
+local function result(t,fmt,a,b,c)
+ if t then
+ report(fmt,a or "done",b or "done",c or "done","done")
+ return t
+ else
+ report(fmt,a or "failed",b or "failed",c or "failed","failed")
+ end
+end
+
+local f = replacer (
+ [[curl ]] ..
+ [[--silent --insecure ]] ..
+ [[-X POST ]] ..
+ [[-H "Authorization: Basic YjAxM2FhMjYtOTcyNC00ZGJkLTg4OTctMDQ4YjlhYWRhMjQ5OnRlc3Q=" ]] ..
+ [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
+ [[-d "Content-Type=application/x-www-form-urlencoded; charset=utf-8" ]] ..
+ [[-d "Host=rs.alarmnet.com/" ]] ..
+ [[-d "Cache-Control=no-store no-cache" ]] ..
+ [[-d "Pragma=no-cache" ]] ..
+ [[-d "grant_type=password" ]] ..
+ [[-d "scope=EMEA-V1-Basic EMEA-V1-Anonymous EMEA-V1-Get-Current-User-Account" ]] ..
+ [[-d "Username=%username%" ]] ..
+ [[-d "Password=%password%" ]] ..
+ [[-d "Connection=Keep-Alive" ]] ..
+ [["https://tccna.honeywell.com/Auth/OAuth/Token"]]
+)
+
+local function getaccesstoken(presets)
+ if validpresets(presets) then
+ local c = presets.credentials
+ local s = c and f {
+ username = c.username,
+ password = c.password,
+ applicationid = applicationid,
+ }
+ local r = s and resultof(s)
+ local t = r and jsontolua(r)
+ return result(t,"getting access token %a")
+ end
+ return result(false,"getting access token %a")
+end
+
+local f = replacer (
+ [[curl ]] ..
+ [[--silent --insecure ]] ..
+ [[-H "Authorization: bearer %accesstoken%" ]] ..
+ [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
+ [[-H "applicationId: %applicationid%" ]] ..
+ [["https://tccna.honeywell.com/WebAPI/emea/api/v1/userAccount"]]
+)
+
+local function getuserinfo(presets)
+ if validpresets(presets) then
+ local c = presets.credentials
+ local s = c and f {
+ accesstoken = c.accesstoken,
+ applicationid = c.applicationid,
+ }
+ local r = s and resultof(s)
+ local t = r and jsontolua(r)
+ return result(t,"getting user info for %a")
+ end
+ return result(false,"getting user info for %a")
+end
+
+local f = replacer (
+ [[curl ]] ..
+ [[--silent --insecure ]] ..
+ [[-H "Authorization: bearer %accesstoken%" ]] ..
+ [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
+ [[-H "applicationId: %applicationid%" ]] ..
+ [["https://tccna.honeywell.com/WebAPI/emea/api/v1/location/installationInfo?userId=%userid%&includeTemperatureControlSystems=True"]]
+)
+
+local function getlocationinfo(presets)
+ if validpresets(presets) then
+ local c = presets.credentials
+ local s = c and f {
+ accesstoken = c.accesstoken,
+ applicationid = applicationid,
+ userid = c.userid,
+ }
+ local r = s and resultof(s)
+ local t = r and jsontolua(r)
+ return result(t,"getting location info for %a")
+ end
+ return result(false,"getting location info for %a")
+end
+
+local f = replacer (
+ [[curl ]] ..
+ [[--silent --insecure ]] ..
+ [[-H "Authorization: bearer %accesstoken%" ]] ..
+ [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
+ [[-H "applicationId: %applicationid%" ]] ..
+ [["https://tccna.honeywell.com/WebAPI/emea/api/v1/temperatureZone/%zoneid%/schedule"]]
+)
+
+local function getschedule(presets,zonename)
+ if validpresets(presets) then
+ local zoneid = presets.data.zones[zonename].zoneId
+ if zoneid then
+ local c = presets.credentials
+ local s = c and f {
+ accesstoken = c.accesstoken,
+ applicationid = applicationid,
+ zoneid = zoneid,
+ }
+ local r = s and resultof(s)
+ local t = r and jsontolua(r)
+ return result(t,"getting schedule for zone %a, %s",zonename or "?")
+ end
+ end
+ return result(false,"getting schedule for zone %a, %s",zonename or "?")
+end
+
+local f = replacer (
+ [[curl ]] ..
+ [[--silent --insecure ]] ..
+ [[-H "Authorization: bearer %accesstoken%" ]] ..
+ [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
+ [[-H "applicationId: %applicationid%" ]] ..
+ [["https://tccna.honeywell.com/WebAPI/emea/api/v1/location/%locationid%/status?includeTemperatureControlSystems=True" ]]
+)
+
+local function getstatus(presets,locationid,locationname)
+ if locationid and validpresets(presets) then
+ local c = presets.credentials
+ local s = c and f {
+ accesstoken = c.accesstoken,
+ applicationid = applicationid,
+ locationid = locationid,
+ }
+ local r = s and resultof(s)
+ local t = r and jsontolua(r)
+ return result(t and t.gateways and t,"getting status for location %a, %s",locationname or "?")
+ end
+ return result(false,"getting status for location %a, %s",locationname or "?")
+end
+
+local function validated(presets)
+ if validpresets(presets) then
+ local data = getlocationinfo(presets)
+ if data and type(data) == "table" and data[1] and data[1].locationInfo then
+ return true
+ else
+ local data = getaccesstoken(presets)
+ if data then
+ presets.credentials.accesstoken = data.access_token
+ local data = getuserinfo(presets)
+ if data then
+ presets.credentials.userid = data.userId
+ return true
+ end
+ end
+ end
+ end
+end
+
+local function findzone(presets,name)
+ if not presets then
+ return
+ end
+ local data = presets.data
+ if not data then
+ return
+ end
+ local usedzones = data.zones
+ return usedzones and usedzones[name]
+end
+
+local function gettargets(zone) -- maybe also for a day
+ local schedule = zone.schedule
+ local min = false
+ local max = false
+ if schedule then
+ local schedules = schedule.dailySchedules
+ if schedules then
+ for i=1,#schedules do
+ local switchpoints = schedules[i].switchpoints
+ for i=1,#switchpoints do
+ local m = switchpoints[i].temperature
+ if not min or m < min then
+ min = m
+ end
+ if not max or m > max then
+ max = m
+ end
+ end
+ end
+ else
+ report("zone %a has no schedule",name)
+ end
+ end
+ return min, max
+end
+
+local function updatezone(presets,name,zone)
+ if not zone then
+ zone = findzone(presets,name)
+ end
+ if zone then
+ local oldtarget = presets.data.states[name]
+ local min = zone.heatSetpointCapabilities.minHeatSetpoint or 5
+ local max = zone.heatSetpointCapabilities.maxHeatSetpoint or 12
+ local mintarget, maxtarget = gettargets(zone)
+ -- todo: maybe get these from presets
+ if mintarget == false then
+ if min < 5 then
+ mintarget = 5
+ -- report("zone %a, min target limited to %a",name,mintarget)
+ else
+ mintarget = min
+ end
+ end
+ if maxtarget == false then
+ if max > 18.5 then
+ maxtarget = 18.5
+ -- report("zone %a, max target limited to %a",name,maxtarget)
+ else
+ maxtarget = max
+ end
+ end
+ local current = zone.temperatureStatus.temperature or 0
+ local target = zone.heatSetpointStatus.targetTemperature
+ local mode = zone.heatSetpointStatus.setpointMode
+ local state = (mode == "FollowSchedule" and "schedule" ) or
+ (mode == "PermanentOverride" and target <= mintarget and "permanent") or
+ (mode == "TemporaryOverride" and target <= mintarget and "off" ) or
+ (mode == "TemporaryOverride" and target >= maxtarget and "on" ) or
+ ( "unknown" )
+ local t = {
+ name = zone.name,
+ id = zone.zoneId,
+ schedule = zone.schedule,
+ mode = mode,
+ current = current,
+ target = target,
+ min = min,
+ max = max,
+ state = state,
+ lowest = mintarget,
+ highest = maxtarget,
+ }
+ -- report("zone %a, current %a, target %a",name,current,target)
+ presets.data.states[name] = t
+ return t
+ end
+end
+
+
+local function geteverything(presets,noschedules)
+ if validated(presets) then
+ local data = getlocationinfo(presets)
+ if data then
+ local usedgateways = presets.data.gateways
+ local usedzones = presets.data.zones
+ for i=1,#data do
+ local gateways = data[i].gateways
+ local locationinfo = data[i].locationInfo
+ local locationid = locationinfo and locationinfo.locationId
+ if gateways and locationid then
+ local status = getstatus(presets,locationid,locationinfo.name)
+ if status then
+ for i=1,#gateways do
+ local gatewaystatus = status.gateways[i]
+ local gatewayinfo = gateways[i]
+ local gatewaysystems = gatewayinfo.temperatureControlSystems
+ local info = gatewayinfo.gatewayInfo
+ local statussystems = gatewaystatus.temperatureControlSystems
+ if gatewaysystems and statussystems and info then
+ local mac = info.mac
+ if usedgateways[mac] then
+ report("%s gateway with mac address %a","using",mac)
+ for j=1,#gatewaysystems do
+ local gatewayzones = gatewaysystems[j].zones
+ local zonestatus = statussystems[j].zones
+ if gatewayzones and zonestatus then
+ for k=1,#gatewayzones do
+ local zonestatus = zonestatus[k]
+ local gatewayzone = gatewayzones[k]
+ if zonestatus and gatewayzone then
+ local zonename = zonestatus.name
+ local zoneid = zonestatus.zoneId
+ if validzonetypes[gatewayzone.zoneType] and zonename == gatewayzone.name then
+ gatewayzone.heatSetpointStatus = zonestatus.heatSetpointStatus
+ gatewayzone.temperatureStatus = zonestatus.temperatureStatus
+ local zonestatus = usedzones[zonename] -- findzone(states,zonename)
+ local schedule = zonestatus and zonestatus.schedule
+ usedzones[zonename] = gatewayzone
+ if schedule and noschedules then
+ gatewayzone.schedule = schedule
+ else
+ gatewayzone.schedule = getschedule(presets,zonename)
+ end
+ updatezone(presets,zonename,gatewayzone)
+ end
+ end
+ end
+ end
+ end
+ else
+ report("%s gateway with mac address %a","skipping",mac)
+ end
+ end
+ end
+ end
+ end
+ end
+ savetable(presets.files.everything,data)
+ return result(data,"getting everything, %s")
+ end
+ end
+ return result(false,"getting everything, %s")
+end
+
+local function gettemperatures(presets)
+ if validated(presets) then
+ local data = loadeverything(presets)
+ if not data or not next(data) then
+ data = geteverything(presets)
+ end
+ if data then
+ local updated = false
+ for i=1,#data do
+ local gateways = data[i].gateways
+ local locationinfo = data[i].locationInfo
+ local locationid = locationinfo.locationId
+ if gateways then
+ local status = getstatus(presets,locationid,locationinfo.name)
+ if status then
+ for i=1,#gateways do
+ local g = status.gateways[i]
+ local gateway = gateways[i]
+ local systems = gateway.temperatureControlSystems
+ if systems then
+ local s = g.temperatureControlSystems
+ for i=1,#systems do
+ local zones = systems[i].zones
+ if zones then
+ local z = s[i].zones
+ for i=1,#zones do
+ if validzonetypes[zone.zoneType] then
+ local z = z[i]
+ if z.name == zone.name then
+ zone.temperatureStatus = z.temperatureStatus
+ updated = true
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ if updated then
+ data.time = ostime()
+ savetable(presets.files.latest,data)
+ end
+ return result(data,"getting temperatures, %s")
+ end
+ end
+ return result(false,"getting temperatures, %s")
+end
+
+local function setmoment(target,time,data)
+ if not time then
+ time = ostime()
+ end
+ local t = osdate("*t",time )
+ local c_year, c_month, c_day, c_hour, c_minute = t.year, t.month, t.day, t.hour, t.min
+ --
+ local years = target.years if not years then years = { } target.years = years end
+ local d_year = years[c_year] if not d_year then d_year = { } years[c_year] = d_year end
+ local months = d_year.months if not months then months = { } d_year.months = months end
+ local d_month = months[c_month] if not d_month then d_month = { } months[c_month] = d_month end
+ local days = d_month.days if not days then days = { } d_month.days = days end
+ local d_day = days[c_day] if not d_day then d_day = { } days[c_day] = d_day end
+ local hours = d_day.hours if not hours then hours = { } d_day.hours = hours end
+ local d_hour = hours[c_hour] if not d_hour then d_hour = { } hours[c_hour] = d_hour end
+ --
+ c_minute = div(c_minute,15) + 1
+ --
+ local d_last = d_hour[c_minute]
+ if d_last then
+ for k, v in next, data do
+ local d = d_last[k]
+ if d then
+ data[k] = (d + v) / 2
+ end
+ end
+ end
+ d_hour[c_minute] = data
+ --
+ target.lasttime = {
+ year = c_year,
+ month = c_month,
+ day = c_day,
+ hour = c_hour,
+ minute = c_minute,
+ }
+end
+
+local function loadtemperatures(presets)
+ if validpresets(presets) then
+ local status = loadeverything(presets)
+ if status then
+ local usedgateways = presets.data.gateways
+ for i=1,#status do
+ local gateways = status[i].gateways
+ if gateways then
+ for i=1,#gateways do
+ local gatewayinfo = gateways[i]
+ local systems = gatewayinfo.temperatureControlSystems
+ local info = gatewayinfo.gatewayInfo
+ if systems and info and usedgateways[info.mac] then
+ for i=1,#systems do
+ local zones = systems[i].zones
+ if zones then
+ local summary = { time = status.time }
+ for i=1,#zones do
+ local zone = zones[i]
+ if validzonetypes[zone.zoneType] then
+ summary[#summary+1] = updatezone(presets,zone.name,zone)
+ end
+ end
+ return result(summary,"loading temperatures, %s")
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ return result(false,"loading temperatures, %s")
+end
+
+local function updatetemperatures(presets)
+ if validpresets(presets) then
+ local everythingname = presets.files.everything
+ local historyname = presets.files.history
+ if everythingname and historyname then
+ gettemperatures(presets,everythingname)
+ local t = loadtemperatures(presets)
+ if t then
+ local data = { }
+ for i=1,#t do
+ local ti = t[i]
+ data[ti.name] = ti.current
+ end
+ local history = loadhistory(historyname) or { }
+ setmoment(history,ostime(),data)
+ savetable(historyname,history)
+ return result(t,"updating temperatures, %s")
+ end
+ end
+ end
+ return result(false,"updating temperatures, %s")
+end
+
+local function getzonestate(presets,name)
+ return validpresets(presets) and presets.data.states[name]
+end
+
+local f = replacer (
+ [[curl ]] ..
+ [[--silent --insecure ]] ..
+ [[-X PUT ]] ..
+ [[-H "Authorization: bearer %accesstoken%" ]] ..
+ [[-H "Accept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml" ]] ..
+ [[-H "applicationId: %applicationid%" ]] ..
+ [[-H "Content-Type: application/json" ]] ..
+ [[-d "%[settings]%" ]] ..
+ [["https://tccna.honeywell.com/WebAPI/emea/api/v1/temperatureZone/%zoneid%/heatSetpoint"]]
+)
+
+local function untilmidnight()
+ local t = osdate("*t")
+ t.hour = 23
+ t.min = 59
+ t.sec = 59
+ return osdate("%Y-%m-%dT%H:%M:%SZ",ostime(t))
+end
+
+local followschedule = {
+ -- HeatSetpointValue = 0,
+ SetpointMode = "FollowSchedule",
+}
+
+local function setzonestate(presets,name,temperature,permanent)
+ local zone = findzone(presets,name)
+ if zone then
+ local m = followschedule
+ if type(temperature) == "number" and temperature > 0 then
+ if permanent then
+ m = {
+ HeatSetpointValue = temperature,
+ SetpointMode = "PermanentOverride",
+ }
+ else
+ m = {
+ HeatSetpointValue = temperature,
+ SetpointMode = "TemporaryOverride",
+ TimeUntil = untilmidnight(),
+ }
+ end
+ end
+ local s = f {
+ accesstoken = presets.credentials.accesstoken,
+ applicationid = applicationid,
+ zoneid = zone.zoneId,
+ settings = jsontostring(m),
+ }
+ local r = s and resultof(s)
+ local t = r and jsontolua(r)
+-- inspect(r)
+-- inspect(t)
+ return result(t,"setting state of zone %a, %s",name)
+ end
+ return result(false,"setting state of zone %a, %s",name)
+end
+
+local function resetzonestate(presets,name)
+ setzonestate(presets,name)
+end
+
+--
+
+local function update(presets,noschedules)
+ local everything = geteverything(presets,noschedules)
+ if everything then
+ presets.data.everything = everything
+ return presets
+ end
+end
+
+local function initialize(filename)
+ local presets = loadpresets(filename)
+ if presets then
+ return update(presets)
+ end
+end
+
+local function off(presets,name)
+ local zone = presets and getzonestate(presets,name)
+ if zone then
+ setzonestate(presets,name,zone.lowest)
+ end
+end
+
+local function on(presets,name)
+ local zone = presets and getzonestate(presets,name)
+ if zone then
+ setzonestate(presets,name,zone.highest)
+ end
+end
+
+local function schedule(presets,name)
+ local zone = presets and getzonestate(presets,name)
+ if zone then
+ resetzonestate(presets,name)
+ end
+end
+
+local function permanent(presets,name)
+ local zone = presets and getzonestate(presets,name)
+ if zone then
+ setzonestate(presets,name,zone.lowest,true)
+ end
+end
+
+-- tasks
+
+local function settask(presets,when,tag,action)
+ if when == "tomorrow" then
+ local list = presets.scheduled
+ if not list then
+ list = loadtable(presets.files.schedules) or { }
+ presets.scheduled = list
+ end
+ if action then
+ list[tag] = {
+ time = ostime() + 24*60*60,
+ done = false,
+ category = category,
+ action = action,
+ }
+ else
+ list[tag] = nil
+ end
+ savetable(presets.files.schedules,list)
+ end
+end
+
+local function gettask(presets,when,tag)
+ if when == "tomorrow" then
+ local list = presets.scheduled
+ if not list then
+ list = loadtable(presets.files.schedules) or { }
+ presets.scheduled = list
+ end
+ return list[tag]
+ end
+end
+
+local function resettask(presets,when,tag)
+ settask(presets,when,tag)
+end
+
+local function checktasks(presets)
+ local list = presets.scheduled
+ if not list then
+ list = loadtable(presets.files.schedules) or { }
+ presets.scheduled = list
+ end
+ if list then
+ local t = osdate("*t")
+ local q = { }
+ for k, v in next, list do
+ local d = osdate("*t",v.time)
+ if not v.done and d.year == t.year and d.month == t.month and d.day == t.day then
+ local a = v.action
+ if type(a) == "function" then
+ a()
+ end
+ v.done = true
+ end
+ if d.year <= t.year and d.month <= t.month and d.day < t.day then
+ q[k] = true
+ end
+ end
+ if next(q) then
+ for k, v in next, q do
+ list[q] = nil
+ end
+ savetable(presets.files.schedules,list)
+ end
+ return list
+ end
+end
+
+-- predefined tasks
+
+local function settomorrow(presets,tag,action)
+ settask(presets,"tomorrow",tag,action)
+end
+
+local function resettomorrow(presets,tag)
+ settask(presets,"tomorrow",tag)
+end
+
+local function tomorrowset(presets,tag)
+ return gettask(presets,"tomorrow",tag) and true or false
+end
+
+--
+
+local evohome
+
+local function poller(presets)
+ --
+ if type(presets) ~= "string" then
+ report("invalid presets file")
+ os.exit()
+ end
+ report("loading presets from %a",presets)
+ local presets = loadpresets(presets)
+ if not validpresets(presets) then
+ report("invalid presets, aborting")
+ os.exit()
+ end
+ --
+ local actions = presets.files.actions
+ if type(actions) ~= "string" then
+ report("invalid actions file")
+ os.exit()
+ end
+ report("loading actions from %a",actions)
+ local actions = loadtable(actions)
+ if type(actions) ~= "table" then
+ report("invalid actions, aborting")
+ os.exit()
+ end
+ actions = actions.actions
+ if type(actions) ~= "table" then
+ report("invalid actions file, no actions subtable")
+ os.exit()
+ end
+ --
+ report("updating device status")
+ update(presets)
+ --
+ presets.report = report
+ presets.evohome = evohome
+ presets.results = { }
+ --
+ function presets.getstate(name)
+ return getzonestate(presets,name)
+ end
+ function presets.tomorrowset(name)
+ return tomorrowset(presets,name)
+ end
+ --
+ local template = actions.template or presets.files.template
+ --
+ local process = function(t)
+ local category = t.category
+ local action = t.action
+ if category and action then
+ local c = actions[category]
+ if c then
+ local a = c[action]
+ if type(a) == "function" then
+ report("category %a, action %a, executing",category,action)
+ presets.results.template = template -- can be overloaded by action
+ a(presets)
+ update(presets,true)
+ else
+ report("category %a, action %a, invalid action, known: %, t",category,action,sortedkeys(c))
+ end
+ else
+ report("category %a, action %a, invalid category, known categories: %, t",category,action,sortedkeys(actions))
+ end
+ else
+ -- logs.report("invalid category and action")
+ end
+ end
+ --
+ local delay = presets.delay or 10
+ local interval = 15 * 60 -- 15 minutes
+ local interval = 60 * 60 -- 60 minutes
+ local refresh = 5 * 60
+ local passed = 0
+ local step = function()
+ if passed > interval then
+ report("refreshing states, every %i seconds",interval)
+ -- todo: update stepwise as this also updates the schedules that we don't really
+ -- change often and definitely not in the middle of the night, so maybe just
+ -- update 9:00 12:00 15:00 18:00 21:00
+ update(presets)
+ passed = 0
+ else
+ passed = passed + delay
+ end
+ checktasks(presets)
+ return delay
+ end
+ --
+ presets.refreshtime = refresh
+ --
+ return step, process, presets
+end
+
+--
+
+evohome = {
+ helpers = {
+ getaccesstoken = getaccesstoken, -- presets
+ getuserinfo = getuserinfo, -- presets
+ getlocationinfo = getlocationinfo, -- presets
+ getschedule = getschedule, -- presets, name
+ --
+ geteverything = geteverything, -- presets, noschedules
+ gettemperatures = gettemperatures, -- presets
+ getzonestate = getzonestate, -- presets, name
+ setzonestate = setzonestate, -- presets, name, temperature
+ resetzonestate = resetzonestate, -- presets, name
+ getzonedata = findzone, -- presets, name
+ --
+ loadpresets = loadpresets, -- filename
+ loadhistory = loadhistory, -- presets | filename
+ loadeverything = loadeverything, -- presets | filename
+ loadtemperatures = loadtemperatures, -- presets | filename
+ --
+ updatetemperatures = updatetemperatures, -- presets
+ },
+ actions= {
+ initialize = initialize, -- filename
+ update = update, -- presets
+ --
+ off = off, -- presets, name
+ on = on, -- presets, name
+ schedule = schedule, -- presets, name
+ permanent = permanent, -- presets, name
+ --
+ settomorrow = settomorrow, -- presets, tag, function
+ resettomorrow = resettomorrow, -- presets, tag
+ tomorrowset = tomorrowset, -- presets, tag
+ --
+ poller = poller, -- presets
+ }
+}
+
+if utilities then
+ utilities.evohome = evohome
+end
+
+-- local presets = evohome.helpers.loadpresets("c:/data/develop/domotica/code/evohome-presets.lua")
+-- evohome.helpers.setzonestate(presets,"Voorkamer",22)
+-- evohome.helpers.setzonestate(presets,"Voorkamer")
+
+return evohome
+