--[[ EventRunner. HC2 scene emulator Copyright (c) 2019 Jan Gabrielsson Email: jan@gabrielsson.com MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. json library - Copyright (c) 2018 rxi https://github.com/rxi/json.lua --]] _version,_fix = "0.11","fix14" --Oct 22, 2019 _sceneName = "HC2 emulator" _LOCAL=true -- set all resource to local in main(), i.e. no calls to HC2 _EVENTSERVER = 6872 -- To receieve triggers from external systems, HC2, Node-red etc. _SPEEDTIME = false -- Run faster than realtime, if set to false run in realtime _MAXTIME = 24*135 -- Max hours to run emulator _BLOCK_PUT=false -- Block http PUT commands to the HC2 - e.g. changing resources on the HC2 _BLOCK_POST=true -- Block http POST commands to the HC2 - e.g. creating resources on the HC2 _AUTOCREATEGLOBALS=true -- Will (silently) autocreate a local fibaro global if it doesn't exist _AUTOCREATEDEVICES=true -- Will (silently) autocreate a local fibaro device if it doesn't exist _VALIDATECHARS = true -- Check rules for invalid characters (cut&paste, multi-byte charqcters) _COLOR = true -- Log with colors on ZBS Output console _HC2_FILE = "HC2.data" -- Default name of data file _HC2_IP=_HC2_IP or "192.198.1.84" -- HC2 IP address _HC2_USER=_HC2_USER or "xxx@yyy" -- HC2 user name _HC2_PWD=_HC2_PWD or "xxxxxx" -- HC2 password _EVENTRUNNER_SUPPORT=true -- Announce presence to HC2 and other ER scenes _DEBUGREMOTERSRC=true _FORCERESOURCEUPDATE=false local creds = loadfile("HC2credentials.lua") -- To not accidently commit credentials to Github... if creds then creds() end -------------------------------------------------------- -- Main, register scenes, create temporary deviceIDs, schedule triggers... -------------------------------------------------------- function main() if HC2.getIPadress():match("192%.168") then -- only of we are on the local network --HC2.copyConfigFromHC2(_HC2_FILE) -- read in configuration HC2 and write to file end HC2.loadConfigFromFile(_HC2_FILE) -- read in HC2 configuration data from file if _LOCAL then -- Set all resources to local HC2.setLocal("devices",true) -- set all devices to local /api/devices HC2.setLocal("virtualDevices",true) -- set all devices to local /api/virtualDevices HC2.setLocal("globalVariables",true) -- set all globals to local /api/globals HC2.setLocal("rooms",true) -- set all rooms to local /api/rooms HC2.setLocal("scenes",true) -- set all scenes to local /api/scenes HC2.setLocal("settings","info") -- set info to local. /api/settings/info HC2.setLocal("settings","location") -- set location to local. /api/settings/location HC2.setLocal("settings","network") -- set location to local. /api/settings/network HC2.setLocal("weather",true) -- set weather to local. /api/weather HC2.setLocal("users",true) -- set users to local. /api/users HC2.setLocal("iosDevices",true) -- set iPhones to local /api/iosDevices end --HC2.setRemote("devices",{1,2}) -- sunset/sunrise/latitude/longitude etc. --HC2.setRemote("devices",{66,88}) -- We still want to run local, except for deviceID 66,88 that will be controlled on the HC2 --HC2.createDevice(88,"Test") --HC2.setRemote("devices",{5}) HC2.loadEmbedded() -- If we are called from another scene (dofile...) local setup = loadfile("HC2setup.lua") -- To not accidently commit credentials to Github... if setup then setup() end --Proxy.installProxy() --Proxy.removeProxy() --HC2.loadScenesFromDir("scenes") -- Load all files with name _.lua from dir, Ex. 11_MyScene.lua --HC2.createDevice(77,"Test") -- Create local deviceID 77 with name "[[Test" --HC2.registerScene("EventRunnerSub3",21,"EventRunnerSub3.lua") --HC2.registerScene("EventRunnerPub3",22,"EventRunnerPub3.lua") --HC2.registerScene("Publisher",32,"EventRunnerPub.lua") --HC2.registerScene("Pars",30,"Pars.lua") --HC2.registerScene("Lib",33,"Lib1.lua") -- Simple test scene --[[ HC2.registerScene("SceneTest",99,"sceneTest.lua",nil, {"+/00:00:02;call(66,'turnOn')", -- breached after 2 sec "+/00:01:02;call(66,'turnOff')"}) -- safe after 1min and 2sec --]] --HC2.runTriggers{"+/00:00;startScene(".._EMULATED.id..")"} -- another way to autostart an embedded scene --HC2.runTriggers{"+/00:00:02;call(54,'turnOn')"} -- EventRunner test scenes --HC2.registerScene("Supervisor",11,"SupervisorEM.lua") --HC2.registerScene("iosLocator",14,"IOSLOcatorEM.lua") -- List known devices and scenes --HC2.listDevices() --HC2.listScenes() -- Test scenes --HC2.registerScene("Scene1",21,"EventRunner.lua") --HC2.registerScene("Scene1",22,"GEA 6.11.lua") --HC2.registerScene("Scene1",23,"Main scene for time based events v1.3.3.lua",{Darkness=0,TimeOfDay='Morning',Latitude="",Longitude="",MarginSunset="0",MarginSunrise="0"}) --Log fibaro:* calls HC2.logFibaroCalls() --Debug filters can be used to trim debug output from noisy scenes... --HC2.addDebugFilter("Memory used:",true) --HC2.addDebugFilter("GEA run since",true) --HC2.addDebugFilter("%.%.%. check running",true) HC2.addDebugFilter("%b<>(.*)") --dofile("HC2verify.lua") end _debugFlags = { threads=false, triggers=true, eventserver=true, hc2calls=true, globals=false, web=true, fcall=true, fglobal=false, fget=false, fother=false } ------------------------------------------------------ -- Context, functions exported to scenes ------------------------------------------------------ function setupContext(id) -- Table of functions and variables available for scenes return { __fibaroSceneId=id, -- Scene ID __threads=0, -- Currently number of running threads _EMULATED=true, -- Check if we run in emulated mode fibaro=Util.copy(fibaro), -- scenes may patch fibaro:*... __fibaro_get_device = __fibaro_get_device, _System=_System, -- Available for debugging tasks in emulated mode dofile=Runtime.dofile, -- Allow dofile for including code for testing, but use our version that sets context loadfile=Runtime.loadfile, os={clock=os.clock,date=osDate,time=osTime,difftime=os.difftime}, json=json, print=print, net = net, api = api, setTimeout=Runtime.setTimeoutContext, clearTimeout=Runtime.clearTimeout, setInterval=Runtime.setIntervalContext, clearInterval=Runtime.clearInterval, urlencode=urlencode, select=select, split=split, tostring=tostring, tonumber=tonumber, table=table, string=string, math=math, pairs=pairs, ipairs=ipairs, pcall=pcall, xpcall=xpcall, error=error, io=io, collectgarbage=collectgarbage, type=type, next=next, bit32=bit32, debug=debug } end ------------------------------------------------------------------------------ -- Startup ------------------------------------------------------------------------------ function startup() -- Intro Log(LOG.WELCOME,"HC2 %s v%s %s",_sceneName,_version,_fix) if _SPEEDTIME then Log(LOG.WELCOME,"Running speedtime") end if _LOCAL then Log(LOG.WELCOME,"Local mode, will not access resources on HC2") end -- Setup watcher for end of time local endTime= osTime()+_MAXTIME*3600 Runtime.speed(_SPEEDTIME) local function cloop() if osTime()>endTime then Log(LOG.SYSTEM,"%s, End of time (%s hours) - exiting",osDate("%c"),_MAXTIME) os.exit() end Runtime.setTimeout(cloop,1000*3600,"Time watch") end cloop() Event.schedule("n/00:00",updateSun) updateSun() -- Setup process manager (calls GUI etc) ProcessManager = Runtime.makeProcessManager() Runtime.idleHandler = ProcessManager.idleHandler -- Setup webserver local ipAddress = HC2.getIPadress() local GUIServer = createWebserver(ipAddress) -- Create webserver GUIServer.createServer("Event server",_EVENTSERVER,GUIhandler,GUIhandler) Log(LOG.LOG,"Web GUI at http://%s:%s/emu/main",ipAddress,_EVENTSERVER) -- Call main local mainPosts={} function HC2.post(event,t) mainPosts[#mainPosts+1]={event,t} end main() -- Post autostart and posts done in main() Event.post({type='autostart'}) -- Post autostart to get things going for _,e in ipairs(mainPosts) do Event.post(e[1],e[2]) end -- Announce to HC2 ER scenes that we are up and running if _EVENTRUNNER_SUPPORT then ER.announceEmulator(ipAddress,_EVENTSERVER) end -- Start running timers Runtime.runTimers() -- Run our simulated threads... os.exit() end ------------------------------------------------------------------------ -- Support functions - don't touch -- requires ------------------------------------------------------------------------ function Support_functions() require('mobdebug').coro() -- Allow debugging of Lua coroutines --mime = require('mime') https = require ("ssl.https") ltn12 = require("ltn12") --json = require("json") socket = require("socket") http = require("socket.http") cfg = require "dist.config" lfs = require("lfs") _ENV = _ENV or _G or {} -- Environment _HCPrompt="[HC2 ]" function printf(...) print(string.format(...)) end -- Lazy printing - should use Log(...) _format=string.format LOG = {WELCOME = "orange",DEBUG = "white", SYSTEM = "Cyan", LOG = "green", ERROR = "Tomato"} -- ZBS colors, works best with dark color scheme http://bitstopixels.blogspot.com/2016/09/changing-color-theme-in-zerobrane-studio.html if _COLOR=='Dark' then _LOGMAP = {orange="\027[33m",white="\027[37m",Cyan="\027[1;43m",green="\027[32m",Tomato="\027[39m"} -- ANSI escape code, supported by ZBS else _LOGMAP = {orange="\027[33m",white="\027[34m",Cyan="\027[35m",green="\027[32m",Tomato="\027[31m"} -- ANSI escape code, supported by ZBS end _LOGEND = "\027[0m" --[[Available colors in Zerobrane for i = 0,8 do print(("%s \027[%dmXYZ\027[0m normal"):format(30+i, 30+i)) end for i = 0,8 do print(("%s \027[1;%dmXYZ\027[0m bright"):format(38+i, 30+i)) end --]] local function prconvert(o) if type(o)=='table' then if o.__tostring then return o.__tostring(o) else return tojson(o) end else return o end end local function tprconvert(args) local r={}; for _,o in ipairs(args) do r[#r+1]=prconvert(o) end return r end Util = Util or {} function Util.Msg(color,message,...) color = _COLOR and _LOGMAP[color] or "" local args = type(... or 42) == 'function' and {(...)()} or {...} --message = #args>0 and _format(message,table.unpack(args)) or message message = #args>0 and _format(message,table.unpack(tprconvert(args))) or prconvert(message) local env,sceneid = Scene.global(),_HCPrompt if env then sceneid = _format("[%s:%s]",env.__fibaroSceneId,env.__orgInstanceNumber) end print(_format("%s%s%s %s%s",color,osOrgDate("%a/%b/%d,%H:%M:%S:",osTime()),sceneid,message,_COLOR and _LOGEND or "")) return message end function Util.isEvent(e) return type(e) == 'table' and e.type end function Util.copy(obj) return Util.transform(obj, function(o) return o end) end function Util.equal(e1,e2) local t1,t2 = type(e1),type(e2) if t1 ~= t2 then return false end if t1 ~= 'table' and t2 ~= 'table' then return e1 == e2 end for k1,v1 in pairs(e1) do if e2[k1] == nil or not Util.equal(v1,e2[k1]) then return false end end for k2,v2 in pairs(e2) do if e1[k2] == nil or not Util.equal(e1[k2],v2) then return false end end return true end function Util.transform(obj,tf) if type(obj) == 'table' then local res = {} for l,v in pairs(obj) do res[l] = Util.transform(v,tf) end return res else return tf(obj) end end function Util.isRemoteEvent(e) return type(e)=='table' and type(e[1])=='string' end function Util.encodeRemoteEvent(e) return {urlencode(json.encode(e)),'%%ER%%'} end function Util.decodeRemoteEvent(e) return (json.decode((urldecode(e[1])))) end function _assert(test,msg,...) if not test then msg = _format(msg,...) error(msg,3) end end function _assertf(test,msg,fun) if not test then msg = _format(msg,fun and fun() or "") error(msg,3) end end function Debug(flag,message,...) if flag then Util.Msg(LOG.DEBUG,message,...) end end function Log(color,message,...) return Util.Msg(color,message,...) end Util.getIDfromEvent={ CentralSceneEvent=function(d) return d.deviceId end,AccessControlEvent=function(d) return d.id end } Util.getIDfromTrigger={ property=function(e) return e.deviceID end, event=function(e) return e.event and Util.getIDfromEvent[e.event.type or ""](e.event.data) end } end ------------------------------------------------------------ -- Webserver GUI --------------------------------------------------------------- function Web_functions() function createWebserver(ipAdress) local self = { ipAdress = ipAdress } local function clientHandler(client,getHandler,postHandler,putHandler) client:settimeout(0,'b') client:setoption('keepalive',true) local ip=client:getpeername() --printf("IP:%s",ip) while true do local l,e,j = client:receive() --print(string.format("L:%s, E:%s, J:%s",l or "nil", e or "nil", j or "nil")) if l then local body,referer,header,e,b local method,call = l:match("^(%w+) (.*) HTTP/1.1") repeat header,e,b = client:receive() --print(string.format("H:%s, E:%s, B:%s",header or "nil", e or "nil", b or "nil")) if b and b~="" then body=b end referer = header and header:match("^[Rr]eferer:%s*(.*)") or referer until header == nil or e == 'closed' if method=='POST' and postHandler then postHandler(method,client,call,body,referer) elseif method=='PUT' and putHandler then putHandler(method,client,call,body,referer) elseif method=='GET' and getHandler then getHandler(method,client,call,body,referer) end --client:flush() client:close() return end coroutine.yield() end end local function socketServer(server,getHandler,postHandler,putHandler) while true do repeat client, err = server:accept() if err == 'timeout' then coroutine.yield() end until err ~= 'timeout' ProcessManager.create(clientHandler,"client",client,getHandler,postHandler,putHandler) end end function self.createServer(name,port,getHandler,postHandler,putHandler) local server,c,err=assert(socket.bind("*", port)) local i, p = server:getsockname() local timeoutCounter = 0 assert(i, p) --printf("http://%s:%s/test",ipAdress,port) server:settimeout(0,'b') server:setoption('keepalive',true) ProcessManager.create(socketServer,"server",server,getHandler,postHandler,putHandler) Log(LOG.LOG,"Created %s at %s:%s",name,self.ipAdress,port) end return self end local GUI_HANDLERS = { ["GET"] = { ["(.*)"] = function(client,ref,body,call) local page = Pages.getPath(call) if page~=nil then client:send(page) return true else return false end end, ["/emu/code/(.*)"]=function(client,ref,body,code) if code then loadstring(urldecode(code))() client:send("HTTP/1.1 302 Found\nLocation: "..(ref or "/emu/triggers").."\n") return true end end, ["/images/(.*)"]=function(client,ref,body,image) -- only small images, so we don't chunk it... move to getPages local f = io.open(image) if not f then error("No such file:"..image) end local src = f:read("*all") local len = string.len(src) client:send("HTTP/1.1 200 OK\nContent-Type: image/jpeg\nContent-Length: "..len.."\n\n") client:send(src) f:close() end, ["/emu/fibaro/(%w+)/(%w+)/?(.-)%?(.*)"]=function(client,ref,body,method,id,action,args) args = args and args~="" and args:match("value=(.+)") or nil if method and fibaro[method] then if action=="" then if args ~= nil and method~="setGlobal" then args=json.decode(urldecode(args)) end --printf("Calling fibaro:%s(%s)",method,id) fibaro[method](fibaro,tonumber(id) and tonumber(id) or id,args) else --printf("Calling fibaro:%s(%s,'%s')",method,id,action,args and _format(", '%s'",args) or "") fibaro[method](fibaro,tonumber(id),action,args) end client:send("HTTP/1.1 302 Found\nLocation: "..(ref or "/emu/main").."\n") return true end return false end, }, ["POST"] = { ["^/api(/.*)"] = function(client,ref,body,call) api.post(call,json.decode(body)) client:send("HTTP/1.1 201 Created\nETag: \"c180de84f991g8\"\n\n") return true end, ["^/trigger/(%-?%d+)"] = function(client,ref,body,id) Event.post({type='other', _id=math.abs(tonumber(id)), _args=json.decode(body)}) client:send("HTTP/1.1 201 Created\nETag: \"c180de84f991g8\"\n\n") return true end, ["^/trigger$"] = function(client,ref,body) e = json.decode(body) Event.post(e) client:send("HTTP/1.1 201 Created\nETag: \"c180de84f991g8\"\n") return true end, }, ["PUT"] = { ["(.*)"] = function(client,ref,body,call) if _debugFlags.web then Log(LOG.LOG,"PUT %s %s",call,body) end client:send("HTTP/1.1 201 Created\nETag: \"c180de84f991g8\"\n") return true end, } } function GUIhandler(method,client,call,body,ref) local stat,res = pcall(function() for p,h in pairs(GUI_HANDLERS[method] or {}) do local match = {call:match(p)} if match and #match>0 then if h(client,ref,body,table.unpack(match)) then return end end end client:send("HTTP/1.1 501 Not Implemented\nLocation: "..(ref or "/emu/triggers").."\n") end) if not stat then local p = Pages.renderError(res) client:send(p) end end end ------------------------------------------------------------------------------ -- Scene support -- load -- start -- kill ------------------------------------------------------------------------------ function Proxy_functions() Proxy = {} function Proxy.installProxy(name) name = name or "_EMULATOR_PROXY" local id,proxy local nproxies=0 local scenes = api.rawGet(false,"/scenes") for _,s in ipairs(scenes) do if s.name==name then id = s.id; proxy = s break end -- already exist, return ID end if not id then proxy = {actions = {devices = {}, groups = {}, scenes = {}}, alexaProhibited = true, autostart = true, --iconID = 0, isLua = true, killOtherInstances = false, killable = true, lua = "", maxRunningInstances = 10, name = name,properties = "", protectedByPIN = false,runConfig = "TRIGGER_AND_MANUAL", triggers = {events = {}, globals = {}, properties = {}, weather = {}}, type = "com.fibaro.luaScene",visible = false} proxy = api.rawPost(false,"/scenes",proxy) id = proxy and type(proxy)=='table' and proxy.id end if id then local trH = {autostart={},properties={},globals={},events={}} for _,tr in ipairs(Scene.getAllLoadedTriggers()) do if tr.type=='property' and HC2.getDevice(tr.deviceID,true)._local == false then trH.properties[#trH.properties+1]=_format("%d %s",tr.deviceID,tr.propertyName) elseif tr.type=='global' and HC2.getGlobal(tr.name,true)._local == false then trH.globals[#trH.globals+1]=tr.name elseif tr.type=='event' and HC2.getDevice(Util.getIDfromTrigger[tr.type](tr),true) then local id = Util.getIDfromTrigger[tr.type](tr) trH.events[#trH.events+1]=_format("%d %s",id,tr.event.type) end end local trH2 = {} for h,v in pairs(trH) do trH2[#trH2+1]="%% "..h for _,t in ipairs(v) do nproxies=nproxies+1; trH2[#trH2+1]=t end end trH2=table.concat(trH2,"\n") proxy.runConfig = "TRIGGER_AND_MANUAL" proxy.maxRunningInstances = 10 proxy.lua = "--[[".."\n"..trH2.."\n".."--]]\n"..[[ local host,port = "]]..HC2.getIPadress()..[[",6872 -- IP and port of emulator local URL = string.format("http://%s:%s/trigger",host,port) local trigger = fibaro:getSourceTrigger() local data = json.encode(trigger) fibaro:debug(data) if trigger.type == 'autostart' or trigger.type=='other' then fibaro:abort() end local req = net.HTTPClient() req:request(URL,{options = {method = 'POST', data=data, timeout=500}, error = function(status) fibaro:debug(json.encode(status)) end}) ]] api.rawPut(false,"/scenes/"..id,proxy) Log(LOG.SYSTEM,"HC2 trigger proxy installed (%s)",nproxies) return id end end function Proxy.removeProxy(name) name = name or "_EMULATOR_PROXY" local scenes = api.rawGet(false,"/scenes") for _,s in ipairs(scenes) do if s.name==name then api.rawDelete(false,"/scenes/"..s.id) Log(LOG.SYSTEM,"HC2 trigger proxy removed") break; end end end end ------------------------------------------------------------------------------ -- Scene support -- load -- start -- kill ------------------------------------------------------------------------------ function Scene_functions() Scene={ scenes={} } _SceneContext = {} -- Map from thread -> environment local mt = {}; mt.__mode = "k"; setmetatable(_SceneContext,mt) -- weak keys (keys are coroutines) function YIELD(ms) local co = coroutine.running() if _SceneContext[co] then BREAKIDLE=true; pcall(function() coroutine.yield(co,(ms and ms > 0 and ms or 100)/1000) end) end end -- If we need to access local scene variables function Scene.global() return _SceneContext[coroutine.running()] end -- global(). function Scene.setGlobal(v,s) _SceneContext[coroutine.running()][v]=s end -- setGlobal('v',42) function Scene.load(name,id,file,fullname) if Scene.scenes[id] then Log(LOG.LOG,"Scene %s already loaded",id) return false end Scene.scenes[id] = Scene.scenes[id] or {} local scene,msg = Scene.scenes[id] scene.name = name scene.fullname=fullname scene.id = id scene.fromFile=true scene.runningInstances = 0 scene._local = true scene.runConfig = "TRIGGER_AND_MANUAL" scene.triggers,scene.lua = Scene.parseHeaders(file,id) scene.isLua = true ER.checkForEventRunner(scene) scene.code,msg=loadfile(file) _assert(scene.code~=nil,"Error in scene file %s: %s",file,msg) Log(LOG.SYSTEM,"Loaded scene:%s, id:%s, file:'%s'",name,id,file) return scene end function Scene.start(scene,event,args) if not scene._local then return end local globals,env = setupContext(scene.id) if nil then -- If we need to intercept access to globals, however it slows down debugging (stepping) local context = { __index = function (t,k) --printf("Get %s=%s",k,globals[k]) return globals[k] end, __newindex = function (t,k,v) --printf("Set %s=%s",k,globals[k]) globals[k] = v end } env = {} setmetatable(env,context) else env=globals end globals._ENV=env globals.__fibaroSceneSourceTrigger = event globals.__fibaroSceneArgs = args globals.__sceneCode = scene.code globals.__fullFileName = scene.fullname globals.__debugName=_format("[%s:%s]",scene.id,scene.runningInstances+1) globals.__sceneCleanup = function(co) if (not scene._terminateMsg) or (scene._terminateMsg and not scene._terminateMsg(scene.id,env.__orgInstanceNumber,env)) then Log(LOG.LOG,"Scene %s terminated (%s)",env.__debugName,co) end scene.runningInstances=scene.runningInstances-1 end local tr = Runtime.setTimeoutContext(function() scene.runningInstances=scene.runningInstances+1 env.__orgInstanceNumber=scene.runningInstances setfenv(scene.code,env) --require('mobdebug').on() scene.code() end, 0,scene.name,env) _SceneContext[tr]=env if Util.isRemoteEvent(args) then args=Util.decodeRemoteEvent(args) end if (not scene._startMsg) or (scene._startMsg and not scene._startMsg(scene.id,scene.runningInstances,env)) then Log(LOG.LOG,"Scene %s started (%s), trigger:%s %s(%s)",globals.__debugName,scene.name,tojson(event),args and tojson(args) or "",tr) end end function Scene.stop(scene) if scene.runningInstances>0 then Runtime.clearAllTimeoutFilter(function(t) return t.env and t.env.__fibaroSceneId==scene.id end) Log(LOG.LOG,"Stopping scene %s (%s)",scene.id, scene.name) else Log(LOG.LOG,"Scene %s not running (%s)",scene.id, scene.name) end end function Scene.checkValidCharsInFile(src,fileName) local lines = split(src,'\r') local function ptr(p) local r={}; for i=1,p+9 do r[#r+1]=' ' end return table.concat(r).."^" end for n,s in ipairs(lines) do s=s:match("^%c*(.*)") local p = s:find("\xEF\xBB\xBF") if p then local err = string.format("Illegal UTF-8 sequence in file:%s\rLine:%3d, %s\r%s",fileName,n,s,ptr(p)) err=err:gsub("%%","%%%%") Log(LOG.ERROR,err) end end end function Scene.parseHeaders(fileName,id) local headers = {} local f = io.open(fileName) if not f then error("No such file:"..fileName) end local src = f:read("*all") f:close() Scene.checkValidCharsInFile(src,fileName) local c = src:match("--%[%[.-%-%-%]%]") local curr = nil if c==nil or c=="" then c = "--%[%[\n%%%% autostart\n%]%]--" end if c and c~="" then c=c:gsub("([\r\n]+)","\n") c = split(c,'\n') for i=2,#c-1 do if c[i]:match("^%%%%") then curr=c[i]:match("%a+"); headers[curr]={} elseif curr then local h = headers[curr] or {} h[#h+1] = c[i] headers[curr]=h end end end local events={} for i=1,headers['properties'] and #headers['properties'] or 0 do local id,name = headers['properties'][i]:match("(%d+)%s+([%a]+)") if id and id ~="" and name and name~="" then events[#events+1]={type='property',deviceID=tonumber(id), propertyName=name} end end for i=1,headers['globals'] and #headers['globals'] or 0 do local name = headers['globals'][i]:match("([%w]+)") if name and name ~="" then events[#events+1]={type='global', name=name} end end for i=1,headers['events'] and #headers['events'] or 0 do local id,t = headers['events'][i]:match("(%d+)%s+(CentralSceneEvent)") if id and id~="" and t and t~="" then events[#events+1]={type='event',event={type='CentralSceneEvent',data={deviceId=tonumber(id)}}} else id,t = headers['events'][i]:match("(%d+)%s+(AccessControlEvent)") if id and id~="" and t and t~="" then events[#events+1]={type='event',event={type='AccessControlEvent',data={id=tonumber(id)}}} end end end if headers['autostart'] then events[#events+1]={type='autostart'} end return events,src end function Scene.getAllLoadedTriggers() Scene.allLoadedTriggers = {} local ts={} for id,scene in pairs(HC2.rsrc.scenes) do if scene.fromFile then for _,t in pairs(scene.triggers) do if t.type=='property' or t.type=='global' or t.type=='event' then local f=true for _,t0 in pairs(ts) do if Util.equal(t0,t) then f= false break end end if f then ts[#ts+1]=t end end end end end Scene.allLoadedTriggers = ts return ts end end ------------------------------------------------------------------------ -- HC2 functions -- Creating and managing HC2 resources ------------------------------------------------------------------------ function HC2_functions() HC2 = { rsrc={} } local function initRsrcses() HC2.rsrc.globalVariables = {} HC2.rsrc.devices = {} HC2.rsrc.virtualDevices = {} HC2.rsrc.iosDevices = {} HC2.rsrc.users = {} HC2.rsrc.scenes = {} HC2.rsrc.sections = {} HC2.rsrc.rooms = {} HC2.rsrc.settings ={} -- HC2.rsrc.weather = {} -- { 1 = weather } HC2.rsrc.count = {glob=0,dev=0,vdev=0,ios=0,users=0,scenes=0,sect=0,rooms=0} end initRsrcses() _IPADDRESS = nil function HC2.getIPadress() if _IPADDRESS then return _IPADDRESS end local someRandomIP = "192.168.1.122" --This address you make up local someRandomPort = "3102" --This port you make up local mySocket = socket.udp() --Create a UDP socket like normal mySocket:setpeername(someRandomIP,someRandomPort) local myDevicesIpAddress, somePortChosenByTheOS = mySocket:getsockname()-- returns IP and Port _IPADDRESS = myDevicesIpAddress == "0.0.0.0" and "127.0.0.1" or myDevicesIpAddress return _IPADDRESS end function HC2.runTriggers(tab) if type(tab)=='string' then tab={tab} end for _,s in ipairs(tab or {}) do local t,cmd = s:match("(.-);(.*)") cmd = loadstring("fibaro:"..cmd) Event.post(cmd,t) end end function HC2.registerSceneTrigger(t,sceneID) local scene = HC2.rsrc.scenes[sceneID] Event.event(t,function(env) Scene.start(scene,env.event) end) end function HC2.registerScene(name,id,file,globVars,triggers,fullname) local scene = Scene.load(name,id,file,fullname) if not scene then return end HC2.rsrc.scenes[id]=scene for _,t in ipairs(scene.triggers) do Log(LOG.SYSTEM,"Scene:%s [ Trigger:%s ]",id,tojson(t)) Event.event(t,function(env) Scene.start(scene,env.event) end) end Event.event({type='other',_id=id}, -- startup event function(env) local event = env.event local args = event._args event._args=nil event._id=nil Scene.start(scene,event,args) end) for name,value in pairs(globVars or {}) do HC2.createGlobal(name,value) end HC2.runTriggers(triggers) end local function patchID(t) local res,c={},0; for k,v in pairs(t) do if type(v)=='table' then res[tonumber(k)]=v; if v._local==nil then v._local=false end c=c+1 end end return res,c end function HC2.copyConfigFromHC2(file) file = file or _HC2_FILE initRsrcses() local rsrc,count = HC2.rsrc,HC2.rsrc.count Log(LOG.SYSTEM,"Reading configuration from H2C...") local vars = api.rawGet(false,"/globalVariables/") for _,v in ipairs(vars) do rsrc.globalVariables[v.name] = v; v._local=false; count.glob=count.glob+1; end local s = api.rawGet(false,"/sections") for _,v in ipairs(s) do rsrc.sections[v.id] = v; v._local=false; count.sect=count.sect+1 end s = api.rawGet(false,"/rooms") for _,v in ipairs(s) do rsrc.rooms[v.id] = v; v._local=false; count.rooms=count.rooms+1 end s = api.rawGet(false,"/devices") for _,v in ipairs(s) do rsrc.devices[v.id] = v; v._local=false; count.dev=count.dev+1 end s = api.rawGet(false,"/virtualDevices") for _,v in ipairs(s) do rsrc.virtualDevices[v.id] = v; v._local=false; count.vdev=count.vdev+1 end s = api.rawGet(false,"/scenes") -- need to retrieve once more to get the Lua code for _,v in ipairs(s) do local scene = api.rawGet(false,"/scenes/"..v.id) rsrc.scenes[v.id] = scene; scene._local=false; count.scenes=count.scenes+1; scene.EventRunner = scene.lua and scene.lua:match(ER.gEventRunnerKey) end s = api.rawGet(false,"/iosDevices") for _,v in ipairs(s) do rsrc.iosDevices[v.id] = v; v._local=false; count.ios=count.ios+1 end rsrc.settings.info = api.rawGet(false,"/settings/info"); rsrc.settings.info._local=false rsrc.settings.location = api.rawGet(false,"/settings/location"); rsrc.settings.location._local=false rsrc.settings.network = api.rawGet(false,"/settings/network"); rsrc.settings.network._local=false rsrc.weather[1] = api.rawGet(false,"/weather"); rsrc.weather[1]._local=false HC2.writeConfigurationToFile(file) Log(LOG.SYSTEM,"Configuration from HC2, Globals:%s, Scenes:%s, Device:%s, virtualDevice:%s, Rooms:%s",count.glob,count.scenes,count.dev,count.vdev,count.rooms) end function HC2.loadConfigFromFile(file) file = file or _HC2_FILE local rsrc,count = HC2.rsrc,HC2.rsrc.count local f = io.open(file) if f then local stat,res = pcall(function() count.glob=0 Log(LOG.SYSTEM,"Reading and decoding configuration from %s",file) rsrc=persistence.load(file); for n,v in pairs(rsrc.globalVariables or {}) do if v._local==nil then v._local=false end; count.glob=count.glob+1 end rsrc.devices,count.dev=patchID(rsrc.devices or {}); rsrc.virtualDevices,count.vdev=patchID(rsrc.virtualDevices or {}); rsrc.scenes,count.scenes=patchID(rsrc.scenes); rsrc.rooms,count.rooms=patchID(rsrc.rooms or {}); rsrc.sections,count.sect=patchID(rsrc.sections); rsrc.iosDevices,count.ios=patchID(rsrc.iosDevices or {}); if not rsrc.settings then rsrc.settings = {} end rsrc.settings.info = rsrc.settings.info or rsrc["settings/info"] or rsrc.info[1] if rsrc.settings.info and not next(rsrc.settings.info) then rsrc.settings.info = nil end if rsrc.settings.info and rsrc.settings.info._local==nil then rsrc.settings.info._local=false end rsrc.settings.location = rsrc.settings.location or rsrc["settings/location"] or rsrc.location[1] if rsrc.settings.location and not next(rsrc.settings.location) then rsrc.settings.location = nil end if rsrc.settings.location and rsrc.settings.location._local==nil then rsrc.settings.location._local=false end rsrc.settings.network = rsrc.settings.network or rsrc["settings/network"] or rsrc.network[1] if rsrc.settings.network and not next(rsrc.settings.network) then rsrc.settings.network = nil end if rsrc.settings.network and rsrc.settings.network._local==nil then rsrc.settings.network._local=false end rsrc.weather = rsrc.weather or {} if not rsrc.weather[1] then rsrc.weather = {rsrc.weather} end if rsrc.weather[1]._local==nil then rsrc.weather[1]._local=false end HC2.rsrc=rsrc HC2.rsrc.count = count end) if not stat then Log(LOG.SYSTEM,"Bad format for HC2 data file (%s)(%s), please re-generate",file,res) end else Log(LOG.SYSTEM,"No HC2 data file found (%s)",file) end Log(LOG.SYSTEM,"Configuration from file, Globals:%s, Scenes:%s, Device:%s, Rooms:%s",count.glob,count.scenes,count.dev,count.rooms) end function HC2.writeConfigurationToFile(file) Log(LOG.SYSTEM,"Writing configuration data to '%s'",file) persistence.store(file, HC2.rsrc); end local function standardInfo() local rsrc= [[{"serialNumber":"HC2-999999","hcName":"Home","mac":"00:22:4d:ab:83:46", "_local":true, "zwaveVersion":"4.33","timeFormat":24,"zwaveRegion":"EU","serverStatus":1550914298, "defaultLanguage":"en","sunsetHour":"18:20","sunriseHour":"05:27","hotelMode":false, "temperatureUnit":"C","batteryLowNotification":false,"smsManagement":false,"date":"07:25 | 28.3.2019","softVersion":"4.530", "beta":false, "currentVersion":{"version":"4.530","type":"stable"}, "installVersion":{"version":"","type":"","status":"","progress":0}, "timestamp":1553754343,"online":true,"updateStableAvailable":false, "updateBetaAvailable":true,"newestStableVersion":"4.530","newestBetaVersion":"4.532"}]] return json.decode(rsrc) end local function standardOne() local rsrc= [[{"properties":{"sunsetHour":"06:00","sunriseHour":"20:00"},"_local":true}]] return json.decode(rsrc) end local function standardLocation() local rsrc= [[{"houseNumber":3,"timezone":"Europe/Stockholm","timezoneOffset":3600,"ntp":true, "_local":true, "ntpServer":"pool.ntp.org","date":{"day":28,"month":3,"year":2019},"time":{"hour":7,"minute":27}, "latitude":61.33,"longitude":19.787,"city":"","temperatureUnit":"C","windUnit":"km/h", "timeFormat":24,"dateFormat":"dd.mm.yy","decimalMark":"."}]] return json.decode(rsrc) end local function standardNetwork() local rsrc= [[{"dhcp":true,"ip":"192.168.1.84","mask":"255.255.255.0","gateway":"192.168.1.1", "dns":"192.168.1.1","remoteAccess":true,"remoteAccessSupport":0}]] return json.decode(rsrc) end local function standardWeather() local rsrc= [[{"Temperature": 9.5,"TemperatureUnit": "C","_local":true, "Humidity": 91.8, "Wind": 11.52, "WindUnit": "km/h", "WeatherCondition": "cloudy", "ConditionCode": 26}]] return json.decode(rsrc) end local autocreate = { globalVariables = function(name) if not _AUTOCREATEGLOBALS then return end Debug(_debugFlags.autocreate,"Autocreating global '%s'",name) return HC2.createGlobal(name) end, devices = function(id) if id > 3 and not _AUTOCREATEDEVICES then return end Debug(_debugFlags.autocreate,"Autocreating deviceID:%s",id) if id==1 then return standardOne() elseif id==2 then return standardTwo() -- FIX else return HC2.createDevice(id) end end, settings = function(t) Debug(_debugFlags.autocreate,"Autocreating /settings/%s",t); return t=='info' and standardInfo() or t=='location' and standardLocation() or t=='network' and standardNetwork() end, weather = function(id) Debug(_debugFlags.autocreate,"Autocreating /weather"); return standardWeather() end, } function HC2.getRsrc(name,id,f) local rsrcs=HC2.rsrc[name] local rsrc=rsrcs[id] if not rsrc and autocreate[name] then-- rsrc doesn't exists rsrc = autocreate[name](id) rsrcs[id]=rsrc end if rsrc and rsrc._local==false and not f then -- remote resource - get it from HC2 local url = "/"..name.."/"..id if name == "weather" then url="/weather" end rsrc = api.rawGet(_DEBUGREMOTERSRC,url) rsrcs[id] = rsrc -- cache it rsrc._local = false end return rsrc end function HC2.getAllRsrc(name,filter) local function getId(n,r) return n=='globalVariables' and r.name or r.id end filter = filter or function() return true end if _FORCERESOURCEUPDATE then local rrs = api.rawGet(false,"/"..name) local lrs = HC2.rsrc[name] for _,r in ipairs(rrs) do local id2 = getId(name,r) if lrs[id2] then r._local = lrs[id2]._local; lrs[id2] = r else lrs[id2] = r; r._local = false end end local res = {} for _,r in pairs(lrs) do res[#res+1]=r end return res end local res,rems,rm = {},{},0 for id,r in pairs(HC2.rsrc[name]) do if r._local then if filter(r) then res[#res+1]=r end else rm=rm+1; rems[id]=r end end local rs={} if rm > 8 then -- Over 5 items, do a get all... rs = api.rawGet(false,"/"..name) else for id,_ in pairs(rems) do res[#rs+1] = api.rawGet(false,"/"..name.."/"..id) end end for _,d in ipairs(rs) do if HC2.rsrc[name][getId(name,d)] then res[#res+1]=d end end return res end function updateSun() local d = HC2.rsrc.devices[1] if d and d._local then local sunrise,sunset= SunCalc.sunCalc() Log(LOG.SYSTEM,"Sunrise:%s Sunset:%s (local calc)",sunrise,sunset) d.properties.sunsetHour=sunset; d.properties.sunriseHour=sunrise end end _DEV_PROP_MAP={["IPAddress"]='ip', ["TCPPort"]='port'} function HC2.getDeviceProperty(deviceID,propertyName) local d = HC2.getVirtualDevice(deviceID,true) or HC2.getDevice(deviceID,true) if not d then return nil end if not d._local then return api.rawGet(_DEBUGREMOTERSRC,"/devices/"..deviceID.."/properties/"..propertyName) else propertyName = d.type=='virtual_device' and _DEV_PROP_MAP[propertyName] or propertyName return {value=d.properties[propertyName],modified=d.modified} end end function HC2.getDevice(id,f) return HC2.getRsrc('devices',id,f) end function HC2.getVirtualDevice(id,f) return HC2.getRsrc('virtualDevices',id,f) end function HC2.getGlobal(id,f) return HC2.getRsrc('globalVariables',id,f) end function HC2.getScene(id,f) return HC2.getRsrc('scenes',id,f) end function HC2.getRoom(id,f) return HC2.getRsrc('rooms',id,f) end function HC2.getSection(id,f) return HC2.getRsrc('sections',id,f) end function HC2.getiosDevice(id,f) return HC2.getRsrc('iosDevices',id,f) end function HC2.getWeather(id,f) return HC2.getRsrc('weather',1,f) end function HC2.getInfo(id,f) return HC2.getRsrc('settings','info',f) end function HC2.getNetwork(id,f) return HC2.getRsrc('settings','network',f) end function HC2.getLocation(id,f) return HC2.getRsrc('settings','location',1,f) end local function getResourceAPI(id,name,filter) if id and id ~= "" then return HC2.getRsrc(name,id),200 else return HC2.getAllRsrc(name,filter),200 end end local vLoadedScenes = function(d) return d and (d._local and Scene.scenes[d.id] or d._local==false) end local URLMap = { ["GET:settings"]=function(path) if path=='location' or path=='info' or path=='network' then return HC2.getRsrc("settings",path),200 else return nil,404 end end, ["GET:weather"]=function(path) return HC2.getRsrc("weather",1),200 end, ["GET:devices"]=function(path) local id,prop=path:match("(%d+)/properties/(.*)$") if tonumber(id) and prop then return HC2.getDeviceProperty(tonumber(id),prop),200 end return getResourceAPI(tonumber(path),'devices') end, ["GET:virtualDevices"]=function(path) return getResourceAPI(tonumber(path),'virtualDevices') end, ["GET:sections"]=function(path) return getResourceAPI(tonumber(path),'sections') end, ["GET:rooms"]=function(path) return getResourceAPI(tonumber(path),'rooms') end, ["GET:users"]=function(path) return getResourceAPI(tonumber(path),'users') end, ["GET:globalVariables"]=function(path) return getResourceAPI(path,'globalVariables') end, ["GET:scenes"]=function(path) if tonumber(path) or path=="" then return getResourceAPI(tonumber(path),'scenes',vLoadedScenes) end local id = path:match("(%d+)/debugMessages") if tonumber(id) then -- return debug messages else return nil,404 end end, ["GET:iosDevices"]=function(path) return getResourceAPI(tonumber(path),'iosDevices') end, ["GET:RGBPrograms"]=function(path) return getResourceAPI(tonumber(path),'RGBPrograms') end, ["POST:devices"]=function(path,data) if path=="" then -- create device else return nil,404 end end, ["POST:virtualDevices"]=function(path,data) if path=="" then local vd = api.rawPost(true,"/virtualDevices",data) vd._local=false HC2.rsrc.virtualDevices[vd.id]=vd return vd else return nil,404 end end, ["PUT:virtualDevices"]=function(path,data) if path~="" and tonumber(path) then local vd = api.rawPut(true,"/virtualDevices/"..path,data) vd._local=false HC2.rsrc.virtualDevices[vd.id]=vd return vd else return nil,404 end end, ["DELETE:virtualDevices"]=function(path,data) if path~="" and tonumber(path) then local vd = api.rawDelete(true,"/virtualDevices/"..path) HC2.rsrc.virtualDevices[tonumber(path)]=nil return 200 else return nil,404 end end, ["PUT:globalVariables"]=function(path,data) if path~="" then __fibaro_set_global_variable(path,data) return 200 else return nil,404 end end, ["POST:globalVariables"]=function(path,data) if path=="" then if not __fibaro_get_global_variable(data.name) then return HC2.createGlobal(data.name,data.value),200 else return nil,406 end else return nil,404 end end, ["PUT:scenes"]=function(path,data) local id = tonumber(path) if id and HC2.getScene(id,true) then HC2.rsrc['scenes'][id]=data return data else return nil,404 end end, ["POST:scenes"]=function(path,data) if path=="" then local rsrcs=HC2.rsrc['scenes'] rsrcs[tonumber(data.id)]=data return data,200 end local id,action=path:match("(%d+)/action/(%w+)$") -- start/stop if not tonumber(id) then return nil,404 end id=math.abs(tonumber(id)) if action=='start' then local scene = HC2.getScene(id,true) if not scene then return end if scene._local then --Scene.start(scene,{type='other'},args) Event.post({type='other',_id=scene.id,_args=data.args}) BREAKIDLE=true end elseif action=='stop' then end return nil,404 end, ["POST:mobile"]=function(path,data) if path=="push" then api.rawPost(true,'/mobile/push',data) return 200 else return 404 end end, ["GET:refreshStates"]=function(path,data) -- api.get("/refreshStates?last=" return api.rawGet(false,'/refreshStates'..path) end } function HC2.apiCall(method,call,data,cType) local base,path = call:match("^/(%w+)/?(.*)$") local p = URLMap[method..":"..base] if p then return p(path,data) else return nil,404 end end function HC2.listDevices(list) local res={} for id,dev in pairs(HC2.rsrc.devices) do if tonumber(id) > 3 then res[#res+1]=string.format("deviceID:%-3d, name:%-20s type:%-30s, value:%-10s", id,dev.name,dev.type,dev.properties.value,dev._local and "local" or "") end end if not list then print(table.concat(res,"\r\n")) end return res end function HC2.listScenes(list) local res={} for id,scene in pairs(HC2.rsrc.scenes) do res[#res+1]=string.format("SceneID :%-3d, name:%-10s %s",id,scene.name,scene._local and "local" or "") end if not list then print(table.concat(res,"\r\n")) end return res end local function setRsrcStatus(t,list,args,tp,filter) filter = filter or function(id) return true end if args==true then for _,d in pairs(list) do if filter(d) then d._local=tp end end elseif type(args)=='table' then for _,id in ipairs(args) do if filter(list[id]) then HC2.getRsrc(t,id,true)._local= tp end end elseif type(args)=='string' then if list[args] then list[args]._local = tp end end end local specResources={weather={"weather",1},info={"settings","info"},location={"settings","location"},network={"settings","network"}} local function setRsrcStatus2(t,args,val,name) if specResources[t] then t,args = table.unpack(specResources[t]) end if type(args)=='number' then args={args} end local stat,res = pcall(function() setRsrcStatus(t,HC2.rsrc[t],args,val,f) end) if not stat then Debug(true,"Err trying to %s(%s,%s) (%s)",name,t,tojson(args),res) end end function HC2.setLocal(t,args) setRsrcStatus2(t,args,true,"setLocal") end function HC2.setRemote(t,args) setRsrcStatus2(t,args,false,"setRemote") end function HC2.createGlobal(name,value) if value~=nil then value=tostring(value) end HC2.rsrc.globalVariables[name]={ name=name, value=value, readOnly=false, isEnum=false, created=osTime(), modified=osTime(), _local=true} return HC2.rsrc.globalVariables[name] end -- Devices tamples for making fakse devices local simDevices = { rollerShutter = [[{"type":"com.fibaro.rollerShutter","baseType":"com.fibaro.actor", "interfaces":["energy","levelChange","power","zwave","zwaveMultiChannelAssociation","zwaveSwitchAll"], "properties":{"parameters":[],"categories":"[\"blinds\"]","configured":true,"dead":"false","energy":"0.00","power":"0.00","value":"99"}, "actions":{"close":0,"open":0,"reconfigure":0,"reset":0,"setValue":1,"startLevelDecrease":0,"startLevelIncrease":0,"stop": 0,"stopLevelChange":0}, }]], plug = [[{"type":"com.fibaro.FGWP102","baseType":"com.fibaro.FGWP","enabled":true, "interfaces":["deviceGrouping","energy","power","zwave","zwaveAlarm","zwaveMultiChannelAssociation"], "properties":{"color":"white","dead":"false","deadReason":"","energy":"264.49","markAsDead":"true","value":"true"}, "actions":{"turnOff":0,"turnOn":0} }]], binarySwitch = [[{"type":"com.fibaro.binarySwitch","baseType":"com.fibaro.actor", "interfaces":["deviceGrouping","light","zwave","zwaveMultiChannelAssociation"], "properties":{"parameters":[],"dead":"false","deadReason":"","isLight":"true","markAsDead":"true","value":"false"}, "actions":{"abortUpdate":1,"reconfigure":0,"retryUpdate":1,"startUpdate":1,"turnOff":0,"turnOn":0,"updateFirmware":1}, }]], rgb = [[{"type":"com.fibaro.FGRGBW441M","baseType":"com.fibaro.colorController", "interfaces":["deviceGrouping","energy","levelChange","light","power","zwave","zwaveConfiguration"], "properties": { "parameters":[],"buttonType": "0","dead": "false","markAsDead": "true", "energy": "0.94","power": "0.00","isLight": "false", "lastColorSet":"0,0,0,0","rememberColor":"true","favoriteProgram":"0","currentProgramID":"0","currentProgram":"0", "r":"0","g":"0","b":"0","w": "0","brightness": "0","color": "0,0,0,0","mode": "0","value": "0", }, "actions": { "associationGet": 1,"associationSet": 2,"getParameter": 1,"reconfigure": 0,"reset": 0, "setB": 1,"setG": 1,"setR": 1,"setW": 1,"setBrightness": 1,"setColor": 1,"setFavoriteProgram": 2, "setParameter": 2,"setValue": 1, "startProgram":1,"stopLevelChange":0,"startLevelDecrease":0,"startLevelIncrease":0,"turnOff":0,"turnOn":0} }]], dimmer = [[{"type":"com.fibaro.multilevelSwitch","baseType":"com.fibaro.binarySwitch", "interfaces":["deviceGrouping","levelChange","light","power","zwave","zwaveConfiguration","zwaveSceneActivation"], "properties":{"parameters":[],"configured":"true","dead":"false","power":"0.00","powerConsumption":"42","sceneActivation":"0","value":"0"}, "actions":{ "associationGet":1,"associationSet":2,"getParameter":1,"setParameter":2,"setValue":1, "startLevelDecrease":0,"startLevelIncrease":0,"stopLevelChange":0,"turnOff":0,"turnOn":0} }]], generic = [[{"type":"com.fibaro.multilevelSwitch","baseType":"com.fibaro.binarySwitch", "interfaces":["deviceGrouping","levelChange","light","power","zwave","zwaveConfiguration","zwaveSceneActivation"], "properties":{"parameters":[],"configured":"true","dead":"false","power":"0.00","armed":"0","powerConsumption":"42", "sceneActivation":"0","value":"0"}, "actions":{ "associationGet":1,"associationSet":2,"getParameter":1,"setParameter":2,"setProperty":2,"setValue":1, "startLevelDecrease":0,"startLevelIncrease":0,"stopLevelChange":0,"turnOff":0,"turnOn":0} }]] } function HC2.createDevice(id,name,t,roomID,value) t = t or "generic" _assert(id,"Can't create device with nil ID") name = name or t..":"..id local d = simDevices[t] _assert(d,"Can't create device, "..tostring(t)) d = json.decode(d) d.id=id d.name=name d.roomID = roomID or false d.enabled=true d.visible=true d._local = true if d.lastBreached then d.lastBreached = osTime() end if d.properties.value~=nil and value~=nil then d.properties.value = value end d.created = osTime() d.modified = osTime() if HC2.rsrc.devices[id] then error(_format("deviceID:%s already exists!",id),3) else HC2.rsrc.devices[id]=d end return d end HC2._debugFilters={} function HC2.addDebugFilter(f,ret) HC2._debugFilters[#HC2._debugFilters+1]={str=f,ret=ret} end function HC2.loadScenesFromDir(path) for file in lfs.dir(path) do if file ~= "." and file ~= ".." then local f = path..'/'..file local id,name = file:match("(%d+)_(.*)%.[Ll][Uu][Aa]") if id and name then HC2.registerScene(name,tonumber(id),f) end end end end function HC2.loadEmbedded() local short_src,org_src = "","" for i=1,1000 do local di = debug.getinfo(i) if not di then break else short_src = di.short_src end end org_src = short_src or org_src if _EMULATED then --short_src=short_src:match("[\\/]?([%.%w_%-]+)$") local name,id if type(_EMULATED)=='table' then name,id = _EMULATED.name,_EMULATED.id if _EMULATED.time then Runtime.setTime(_EMULATED.time) end if _EMULATED.maxtime then _MAXTIME=_EMULATED.maxtime end else name,id = short_src:match("(%d+)_(%w+)%.[lL][uU][aA]$") if name then id=tonumber(id) else name,id="Test",99 end end local HC2file = debug.getinfo(1).short_src:match("[\\/]?([%.%w_%-]+)$") local attr1, err1 = lfs.attributes(HC2file) local attr2, err2 = lfs.attributes(short_src) if err1 or err2 then Log(LOG.LOG,"HC2 file name:%s",debug.getinfo(1).short_src) Log(LOG.LOG,"Embedded file name:%s",org_src) error("File load error: "..(err1 or err2).." in "..lfs.currentdir()) end local wd = lfs.currentdir() if wd then wd = wd..(cfg.arch == "Windows" and "\\" or "/")..short_src end local scene = HC2.registerScene(name,id,short_src,nil,nil,wd) end end function HC2.logFibaroCalls() fibaro._logFibaroCalls() end HC2._monitors={} function HC2.monitorDevice(id,prop) if HC2._monitors[id] then return end -- already monitoring prop = prop or "value" local d = HC2.getDevice(id,true) if not d then Log(LOG.WARNING,"Err: monitoring non-existant deviceID:%s",id) return end if d._local then Log(LOG.WARNING,"Err: can't monitor local deviceID:%s",id) return end HC2._monitors[id]=true local val = nil local function monitor() local nv = fibaro:getValue(id,prop) --printf("T:%s, VAL:%s, NVAL:%s",osDate("%c"),tostring(val),tostring(nv)) if nv ~= val then val = nv Event.post({type='property', deviceID=id, propertyName=prop}) end Runtime.setTimeout(monitor,1000,"MON") -- Check every second end monitor() end function HC2.monitorGlobal(global) if HC2._monitors[global] then return end -- already monitoring local d = HC2.getGlobal(global,true) if not d then Log(LOG.WARNING,"Err: monitoring non-existant global:'%s'",global) return end if d._local then Log(LOG.WARNING,"Err: can't monitor local global:'%s'",global) return end HC2._monitors[global]=true local val = nil local function monitor() local nv = fibaro:getGlobalValue(global) --printf("T:%s, VAL:%s, NVAL:%s",osDate("%c"),tostring(val),tostring(nv)) if nv ~= val then val = nv Event.post({type='global', name=global}) end Runtime.setTimeout(monitor,1000,"MON") -- Check every second end monitor() end end ------------------------------------------------------------------------------ -- Runtime ------------------------------------------------------------------------------ function Runtime_functions() Runtime = {} function Runtime.dofile(file) local code,tt = loadfile(file) if code then setfenv(code,_SceneContext[coroutine.running()]) code() else Log(LOG.ERROR,"Missing file or error in file:%s",file) end end function Runtime.loadfile(file) local code = loadfile(file) if code then return function() setfenv(code,_SceneContext[coroutine.running()]) code() end end end function Runtime.getInstance(id,inst) for co,env in pairs(_SceneContext) do if env.__fibaroSceneId==id and env.__orgInstanceNumber==inst then return co,env end end end local _gTimers = nil function Runtime.insertCoroutine(co) if _gTimers == nil then _gTimers=co elseif co.time < _gTimers.time then _gTimers,co.next=co,_gTimers else local tp = _gTimers while tp.next and tp.next.time <= co.time do tp=tp.next end co.next,tp.next=tp.next,co end return co.co end function Runtime.dumpTimers() local t = _gTimers while t do printf("Timer %s at %s",t.name,osOrgDate("%X",t.time)) t=t.next end end osOrgTime,osOrgDate = os.time,os.date _gOffset=0 function osTime(t) return math.floor(osTimeFrac(t)+0.5) end function osTimeFrac(t) return t and osOrgTime(t) or _gTime + _gOffset end function osDate(f,t) return osOrgDate(f,t or osTime()) end _gTime = osOrgTime() function Runtime.setTime(start) local offs=0 if type(start)=='string' then local h,m,s = start:match("(%d+):(%d+):?(%d*)") local d = osOrgDate("*t") d.hour,d.min,d.sec=h,m,s and s~="" and s or 0 offs = osOrgTime(d) elseif type(start)=='number' then offs=osOrgTime()+start end _gTime = osOrgTime() _gOffset=offs-osOrgTime() end WAITINDEX=_SPEEDTIME and "SPEED" or "NORMAL" Runtime.waitUntil={ ["SPEED"] = function(t) _gTime=t-_gOffset if Runtime.idleHandler then Runtime.idleHandler() end return false end, ["NORMAL"] = function(t) local idle = Runtime.idleHandler BREAKIDLE=false local wt = t-(_gTime+_gOffset) local sec = math.floor(wt) local secn = osOrgTime() local _,ms = math.modf(wt) --Log(LOG.LOG,"WAIT:"..sec) while osOrgTime()-(secn+sec) < 0 and not BREAKIDLE do socket.sleep(0.01) if idle then idle() end end --Log(LOG.LOG,"WAITED:"..osOrgTime()-secn) _gTime=_gTime+(osOrgTime()-secn) local ttt = osOrgTime() --Log(LOG.LOG,"WAITE2:"..ms) local ms2 = ms while ms2 > 0 and not BREAKIDLE do if ms2 > 0.01 then socket.sleep(0.01); ms2=ms2-0.01 end local ct = os.clock() if idle then idle() end ms2=ms2-(os.clock()-ct) end --Log(LOG.LOG,"WAITED2:"..osOrgTime()-ttt) _gTime=_gTime+ms return false end, } local speedTimer = nil function Runtime.speed(flag) if flag==nil then return WAITINDEX end flag = flag==false and 0 or flag==true and (_MAXTIME+1)*3600 or flag -- Convert flag to sec to speed, or 0 if type(flag)=='string' then flag=Event.str2time(flag) if flag > osTime() then flag=flag-osTime() end end if speedTimer then Runtime.clearTimeout(speedTimer); speedTimer=nil end if flag > 0 then Runtime.setTimeout(function() Runtime.speed(false) end,flag*1000) end WAITINDEX= flag>0 and 'SPEED' or 'NORMAL' BREAKIDLE=true end function Runtime.runTimers() while _gTimers ~= nil do --Runtime.dumpTimers() ::REDO:: local co,now = _gTimers,osTimeFrac() if co.time > now then Runtime.waitUntil[WAITINDEX](co.time) goto REDO end local ct = os.clock() _gTimers=_gTimers.next if co.env then setfenv(co.env.__sceneCode,co.env) end local stat,thread,time=coroutine.resume(co.co) if not stat then local name=co.name or co.env and "Scene:"..co.env.__fibaroSceneId or tostring(co.co) Log(LOG.ERROR,"Error in %s %s",name,tojson(thread)) print(debug.traceback()) end if time~='%%ABORT%%' and coroutine.status(co.co)=='suspended' then co.time,co.next=osTimeFrac()+time,nil Runtime.insertCoroutine(co) elseif co.env then local t = co.env.__threads co.env.__threads=t-1 t=t-1 if _debugFlags.threads then Log(LOG.LOG,"Dead thread %s, %s, t=%s",co.name or "", co.co,t) end if time=='%%ABORT%%' then local t0 = Runtime.clearAllTimeoutFilter(function(t) return t.env==co.env end) if _debugFlags.scenes then Log(LOG.LOG,"Aborting, %s, %s, t=%s, t0=%s",co.name or "", co.co,t,t0) end t=0 end if t<=0 and co.env.__sceneCleanup then co.env.__sceneCleanup(co.co) end if co.cleanup then co.cleanup() end end --Log(LOG.LOG,"COMP:"..(os.clock()-ct)) _gTime = _gTime+(os.clock()-ct) end Log(LOG.SYSTEM,"%s:End of time(rs)",osOrgDate("%X",osTime())) end function Runtime.setTimeoutContext(fun,time,name,env,cleanup) time = (time or 0)/1000+osTimeFrac() local co = coroutine.create(fun) local cco = coroutine.running() env = env or _SceneContext[cco] _SceneContext[co]=env if env then local t = env.__threads if _debugFlags.threads then Log(LOG.LOG,"Starting thread %s, %s, t=%s",name or "", co,t+1) end env.__threads=t+1 end return Runtime.insertCoroutine({co=co,time=time,name=name,env=env,cleanup=cleanup}) end function Runtime.setTimeout(fun,time,name,context,cleanup) time = (time or 0)/1000+osTimeFrac() local co = coroutine.create(fun) return Runtime.insertCoroutine({co=co,time=time,name=name,context=nil,cleanup=cleanup}) end function Runtime.clearTimeout(timer) if timer==nil then return end if _gTimers.co == timer then _gTimers = _gTimers.next else local tp = _gTimers while tp and tp.next do if tp.next.co == timer then tp.next = tp.next.next return end tp = tp.next end end end function Runtime.clearAllTimeoutFilter(filter,c) c=c or 0 if _gTimers==nil then return c end if filter(_gTimers) then _gTimers = _gTimers.next return Runtime.clearAllTimeoutFilter(filter,c+1) else local tp = _gTimers while tp and tp.next do if filter(tp.next) then tp.next = tp.next.next c=c+1 end tp = tp.next end return c end end function Runtime.setIntervalContext(fun,ms,...) local t0,args,ref=osTime()*1000,{...},{'%%INTERV%%',nil} local function loop() fun(table.unpack(args)) t0=t0+ms ref[2]=Runtime.setTimeoutContext(loop,t0-1000*osTime()) end loop() return ref end function Runtime.setInterval(fun,ms,...) local t0,args,ref=osTime()*1000,{...},{'%%INTERV%%',nil} local function loop() fun(table.unpack(args)) t0=t0+ms ref[2]=Runtime.setTimeout(loop,t0-1000*osTime()) end loop() return ref end function Runtime.clearInterval(ref) _assert(type(ref)=='table' and ref[1]=='%%INTERV%%',"Bad interval reference") if ref[2] then Runtime.clearTimeout(ref[2]) end end function Runtime.makeProcessManager() local self = {} local threads=nil local free=nil local function PP(p,t) printf("%sProcess:%s %s %s %s",p,t.name,t.thread,coroutine.status(t.thread),t.args[1]:getpeername()) end function self.create(fun,name,...) local args={...} fun = coroutine.create(fun) local l=free; if free==nil then l={} else free=free.next end l.thread=fun; l.args=args; l.name=name; l.next=nil if threads==nil then threads=l; return l end local t=threads; while t.next do t=t.next end t.next=l; return l; end local function dispose(t) t.next=free; free=t end local function resume(co,args,name) coroutine.resume(co,table.unpack(args)) return coroutine.status(co) end function self.idleHandler() while(threads) do if resume(threads.thread,threads.args,threads.name)=='dead' then local l = threads; threads=threads.next; dispose(l) else break end end local t = threads while(t and t.next) do if resume(t.next.thread,t.next.args,t.name)=='dead' then local l = t.next; t.next=t.next.next; dispose(l) else t=t.next end end end return self end end ------------------------------------------------------------------------------ -- System ------------------------------------------------------------------------------ function System_functions() _System = {} _System.createGlobal = HC2.createGlobal _System.createDevice = HC2.createDevice _System.setLocal = HC2.setLocal _System.setRemote = HC2.setRemote _System.autoDevices = function(flag) local last=_AUTOCREATEDEVICES; _AUTOCREATEDEVICES=flag; return last end _System.autoGlobals = function(flag) local last=_AUTOCREATEGLOBALS; _AUTOCREATEGLOBALS=flag; return last end _System.blockPut = function(bool) local last=_BLOCK_PUT; _BLOCK_PUT=flag; return last end _System.blockPost = function(bool) local last=_BLOCK_POST; _BLOCK_POST=flag; return last end _System.registerScene = HC2.registerScene function _System.loadScene(name,id,file) HC2.registerScene(name,id,file) local s = Scene.scenes[id] for _,t in ipairs(s.triggers) do if t.type=='autostart' then fibaro:startScene(id) return end end end _System.runTriggers = HC2.runTriggers _System.post = Event.post _System.fib = {} function _System.fib.call(t,id,...) Event.post({type='%FIB%',args={'call',id,...}},t) end function _System.fib.setGlobal(t,name,value) Event.post({type='%FIB%',args={'setGlobal',name,value}},t) end _System.monitorDevice = HC2.monitorDevice _System.monitorGlobal = HC2.monitorGlobal _System.installProxy = Proxy.installProxy _System.removeProxy = Proxy.removeProxy _System.getInstance = Runtime.getInstance _System.reverseMapDef = traceFibaroIDs _System.port = _EVENTSERVER _System.ipAdress = HC2.getIPadress() _System.time = osOrgTime _System.registerSceneTrigger = HC2.registerSceneTrigger _System.speed = Runtime.speed _System._Msg = Util.Msg local function gybdow(tm) local ybdow = tonumber(os.date("%w",os.time{year=os.date("*t",tm).year,month=1,day=1})) return ybdow == 0 and 7 or ybdow end local function getDayAdd(tm) local ybdow = gybdow(tm) return ybdow < 5 and (ybdow - 2) or (ybdow - 9) end function _System.getWeekNumber(tm) local dayOfYear,dayAdd,weekNum = os.date("%j",tm),getDayAdd(tm) local doyc = dayOfYear + dayAdd if(doyc < 0) then dayAdd = getDayAdd(os.time{year=os.date("*t",tm).year-1,month=1,day=1}) dayOfYear = dayOfYear + os.date("%j",os.time{year=os.date("*t",tm).year-1,month=12,day=31}) doyc = dayOfYear + dayAdd end weekNum = math.floor(doyc/7) + 1 if doyc > 0 and weekNum == 53 then local ybdow = gybdow(os.time{year=os.date("*t",tm).year+1,month=1,day=1}) if ybdow < 5 then weekNum = 1 end end return weekNum end end ------------------------------------------------------------------------------ -- Event engine ------------------------------------------------------------------------------ function Event_functions() function createEventEngine() local self,_handlers = { RULE='%%RULE%%' },{} local function _coerce(x,y) local x1 = tonumber(x) if x1 then return x1,tonumber(y) else return x,y end end local _constraints = {} _constraints['=='] = function(val) return function(x) x,val=_coerce(x,val) return x == val end end _constraints['>='] = function(val) return function(x) x,val=_coerce(x,val) return x >= val end end _constraints['<='] = function(val) return function(x) x,val=_coerce(x,val) return x <= val end end _constraints['>'] = function(val) return function(x) x,val=_coerce(x,val) return x > val end end _constraints['<'] = function(val) return function(x) x,val=_coerce(x,val) return x < val end end _constraints['~='] = function(val) return function(x) x,val=_coerce(x,val) return x ~= val end end _constraints[''] = function(val) return function(x) return x ~= nil end end function self._compilePattern(pattern) if type(pattern) == 'table' then if pattern._var_ then return end for k,v in pairs(pattern) do if type(v) == 'string' and v:sub(1,1) == '$' then local var,op,val = v:match("$([%w_]*)([<>=~]*)([+-]?%d*%.?%d*)") var = var =="" and "_" or var local c = _constraints[op](tonumber(val)) pattern[k] = {_var_=var, _constr=c, _str=v} else self._compilePattern(v) end end end end function self._match(pattern, expr) local matches = {} local function _unify(pattern,expr) if pattern == expr then return true elseif type(pattern) == 'table' then if pattern._var_ then local var, constr = pattern._var_, pattern._constr if var == '_' then return constr(expr) elseif matches[var] then return constr(expr) and _unify(matches[var],expr) -- Hmm, equal? else matches[var] = expr return constr(expr) end end if type(expr) ~= "table" then return false end for k,v in pairs(pattern) do if not _unify(v,expr[k]) then return false end end return true else return false end end return _unify(pattern,expr) and matches or false end local toHash,fromHash={},{} fromHash['property'] = function(e) return {e.type..e.deviceID,e.type} end fromHash['global'] = function(e) return {e.type..e.name,e.type} end toHash['property'] = function(e) return e.deviceID and 'property'..e.deviceID or 'property' end toHash['global'] = function(e) return e.name and 'global'..e.name or 'global' end local equal,isEvent = Util.equal,Util.isEvent function self.event(e,action) -- define rules - event template + action _assertf(isEvent(e) or type(e)=='function', "bad event format '%s'",tojson(e)) self._compilePattern(e) local hashKey = toHash[e.type] and toHash[e.type](e) or e.type _handlers[hashKey] = _handlers[hashKey] or {} local rules = _handlers[hashKey] local rule,fn = {[self.RULE]=e, action=action}, true for _,rs in ipairs(rules) do -- Collect handlers with identical patterns. {{e1,e2,e3},{e1,e2,e3}} if equal(e,rs[1][self.RULE]) then rs[#rs+1] = rule fn = false break end end if fn then rules[#rules+1] = {rule} end rule.enable = function() rule._disabled = nil return rule end rule.disable = function() rule._disabled = true return rule end return rule end function self._handleEvent(e) -- running a posted event local env, _match = {event = e, p={}}, self._match local hasKeys = fromHash[e.type] and fromHash[e.type](e) or {e.type} for _,hashKey in ipairs(hasKeys) do for _,rules in ipairs(_handlers[hashKey] or {}) do -- Check all rules of 'type' local match = _match(rules[1][self.RULE],e) if match then if next(match) then for k,v in pairs(match) do env.p[k]=v match[k]={v} end env.context = match end for _,rule in ipairs(rules) do if not rule._disabled then env.rule = rule rule.action(env) end end end end end end local function midnight() local t = osDate("*t"); t.hour,t.min,t.sec = 0,0,0; return osTime(t) end local function hm2sec(hmstr) local offs,sun sun,offs = hmstr:match("^(%a+)([+-]?%d*)") if sun and (sun == 'sunset' or sun == 'sunrise') then hmstr,offs = fibaro:getValue(1,sun.."Hour"), tonumber(offs) or 0 end local sg,h,m,s = hmstr:match("^(%-?)(%d+):(%d+):?(%d*)") _assert(h and m,"Bad hm2sec string %s",hmstr) return (sg == '-' and -1 or 1)*(h*3600+m*60+(tonumber(s) or 0)+(offs or 0)*60) end local function toTime(time) if type(time) == 'number' then return time end local p = time:sub(1,2) if p == '+/' then return hm2sec(time:sub(3))+osTime() elseif p == 'n/' then local t1,t2 = midnight()+hm2sec(time:sub(3)),osTime() return t1 > t2 and t1 or t1+24*60*60 elseif p == 't/' then return hm2sec(time:sub(3))+midnight() else return hm2sec(time) end end function self.post(e,time) -- time in 'toTime' format, see below. _assertf(type(e) == "function" or isEvent(e), "Bad event format %s",function() tojson(e) end) time = toTime(time or osTime()) if time < osTime() then return nil end if _debugFlags.triggers and not (type(e)=='function') then if e.type=='other' and e._id then Log(LOG.LOG,"System trigger:{\"type\":\"other\"} to scene:%s at %s",e._id,osDate("%a %b %d %X",time)) else Log(LOG.LOG,"System trigger:%s at %s",tojson(e),osDate("%a %b %d %X",time)) end end BREAKIDLE=true if type(e)=='function' then return Runtime.setTimeout(e,1000*(time-osTime()),"Timer") else return Runtime.setTimeout(function() self._handleEvent(e) end,1000*(time-osTime()),"Main") end end function self.schedule(time,fun) local function loop() fun() Runtime.setTimeout(loop,1000*(toTime(time)-osTime())) end Runtime.setTimeout(loop,1000*(toTime(time)-osTime())) end self.str2time = toTime return self end Event = createEventEngine() Event.event({type='%FIB%'},function(env) fibaro[env.event.args[1]](fibaro,select(2,table.unpack(env.event.args))) end ) Event.event({type='%%SPEED%%', value='$hours'}, function(env) if not _SPEEDTIME then WAITINDEX="SPEED" BREAKIDLE=true setTimeout(function() WAITINDEX="NORMAL" BREAKIDLE=true end,env.p.hours*60*60*1000) end end) end ------------------------------------------------------------------------------ -- Fibaro functions -- -- Adoption of FibaroSceneAPI.lua ... -- -- Credits: -- Edits by @petergebruers 2017-02-09: -- fix HC user authentication (was: user:password in URL, is now: basic authentication). -- fix chunked responses (was: use only chunk 1, is now: concatenate chunks). Fixes "getDevicesId". -- add error checking and display in the HTTP part, to get sensible error messages. -- Based on a version published by @riemers: -- https://forum.fibaro.com/index.php?/topic/24319-tutorial-zerobrane-usage-lua-coding/ -- And that's based on: -- https://www.domotique-fibaro.fr/topic/9248-zerobrainstudio-pour-ecrire-et-tester-vos-scripts-lua-directement-sur-votre-pc/ -- fibaro:getSourceTrigger() -- fibaro:getSourceTriggerType() -- fibaro:debug() -- fibaro:countScenes([sceneID]) -- fibaro:startScene(sceneID[,args]) -- fibaro:args() -- fibaro:stopScene(sceneID) -- fibaro:sleep(time) -- fibaro:abort() -- fibaro:call(deviceID,method,...) -- fibaro:killScenes(sceneID) -- not yet implemented -- fibaro:isSceneEnabled(sceneID) -- fibaro:setSceneEnabled(sceneID, enabled) -- fibaro:getSceneRunConfig(sceneID) -- fibaro:setSceneRunConfig(sceneID, runConfig) -- fibaro:getRoomID(deviceID) -- fibaro:getSectionID(deviceID) -- fibaro:getType(deviceID) -- fibaro:calculateDistance(position1 , position2) -- fibaro:getName(deviceID) -- fibaro:getRoomName(roomID) -- fibaro:getRoomNameByDeviceID(deviceID) -- fibaro:wakeUpDeadDevice(deviceID) -- only remote -- fibaro:getDevicesId(filter) -- fibaro:getAllDeviceIds() -- fibaro:getIds(devices) -- fibaro:get(deviceID, propertyName) -- fibaro:getValue(deviceID, propertyName) -- fibaro:getModificationTime(deviceID ,propertyName) -- fibaro:getGlobal(varName) -- fibaro:getGlobalValue(varName) -- fibaro:getGlobalModificationTime(varName) -- fibaro:setGlobal(varName ,value) -- fibaro:setLedBrightness -- not implemented yet -- fibaro:getLedBrightness -- not implemented yet -- HomeCenter -- only remote -- setTimeout(function,time) -- clearTimeout(ref) -- net() -- api() -- split(string,char) -- urlencode(string) ------------------------------------------------------------------------------ function Fibaro_functions() fibaro={} function __assert_type(value ,typeOfValue) if type(value) ~= typeOfValue then error("Assertion failed: Expected "..typeOfValue ,3) end end function __convertToString(value) if type(value) == 'boolean' then return value and '1' or '0' elseif type(value) == 'number' then return tostring(value) elseif type(value) == 'table' then return json.encode(value) else return value end end function __fibaroSleep(n) return coroutine.yield(coroutine.running(),n/1000) end function __fibaro_get_device(deviceID,r) local d = HC2.getVirtualDevice(deviceID,r) or HC2.getDevice(deviceID,r) if d then return d,200 else return nil,404 end end function __fibaro_get_device_property(deviceID ,propertyName) return HC2.getDeviceProperty(deviceID,propertyName) end function __fibaro_get_scene(sceneID,r) return HC2.getScene(sceneID,r) end function __fibaro_get_global_variable(name,r) return HC2.getGlobal(name,r) end function __fibaro_get_room(roomID) return HC2.getRoom(roomID) end function __fibaro_set_global_variable(varName,data) local globalVar = __fibaro_get_global_variable(varName,true) if globalVar and globalVar._local then -- we have a local globalVar.value,globalVar.modified=data.value,osTime() if data.invokeScenes then Event.post({type='global',name=globalVar.name}) -- trigger end elseif globalVar then -- we have a remote global if _BLOCK_PUT then error(_format("Trying to update HC2 global %s, set _BLOCK_PUT=false to allow",varName)) else api.rawPut(true,"/globalVariables/"..varName ,data) end else error("Non existent fibaro global: "..varName) -- should we throw an error? end end function fibaro:getSourceTrigger() return Scene.global().__fibaroSceneSourceTrigger end function fibaro:getSourceTriggerType() return Scene.global().__fibaroSceneSourceTrigger["type"] end function fibaro:debug(str) str=tostring(str) for _,f in ipairs(HC2._debugFilters) do local m = str:match(f.str) if m then if f.ret then return else str=m; break end end end local env = Scene.global() print(_format("%s%s %s",osDate("%a/%b/%d,%H:%M:%S:"),env and env.__debugName or "INTERN",str)) end function fibaro:sleep(n) __fibaroSleep(n) end function fibaro:abort() coroutine.yield(coroutine.running(),'%%ABORT%%') end function fibaro:countScenes(sceneID) YIELD() local scene = __fibaro_get_scene(sceneID or Scene.global().__fibaroSceneId) return scene == nil and 0 or scene.runningInstances end function fibaro:isSceneEnabled(sceneID) YIELD() local scene = __fibaro_get_scene(sceneID) return scene and (scene.runConfig == "TRIGGER_AND_MANUAL" or scene.runConfig == "MANUAL_ONLY") end function fibaro:killScenes(sceneID) YIELD() local scene = __fibaro_get_scene(sceneID,true) if not scene then return end if scene._local then Scene.stop(scene) else api.rawPost(true,"/scenes/"..sceneID.."/action/stop") end end fibaro.stopScene = fibaro.killScenes -- symetri and used by web GUI function fibaro:startScene(sceneID,args) YIELD() local scene = __fibaro_get_scene(sceneID,true) if not scene then return end if scene._local then Event.post({type='other',_id=scene.id,_args=args}) else api.rawPost(true,"/scenes/"..sceneID.."/action/start",args and {args=args} or nil) end end function fibaro:args() return Scene.global().__fibaroSceneArgs end function fibaro:setSceneEnabled(sceneID , enabled) YIELD() __assert_type(sceneID ,"number") __assert_type(enabled ,"boolean") local scene = __fibaro_get_scene(sceneID,true) if not scene then return end local runConfig = enabled == true and "TRIGGER_AND_MANUAL" or "DISABLED" if scene._local then scene.runConfig = runConfig else if _BLOCK_PUT then error(_format("Trying to update HC2 scene %s, set _BLOCK_PUT=false to allow",sceneID)) else api.rawPut(true,"/scenes/"..sceneID, {id = sceneID ,runConfig = runConfig}) end end end function fibaro:getSceneRunConfig(sceneID) YIELD() local scene = __fibaro_get_scene(sceneID) if not scene then return nil end return scene.runConfig end function fibaro:setSceneRunConfig(sceneID ,runConfig) YIELD() __assert_type(sceneID ,"number") __assert_type(runConfig ,"string") local scene = __fibaro_get_scene(sceneID,true) if not scene then return end if scene._local then scene.runConfig = runConfig else if _BLOCK_PUT then error(_format("Trying to update HC2 scene %s, set _BLOCK_PUT=false to allow",sceneID)) else api.rawPut(true,"/scenes/"..sceneID, {id = sceneID ,runConfig = runConfig}) end end end function fibaro:getRoomID(deviceID) local dev = __fibaro_get_device(deviceID) if dev == nil then return nil end return dev.roomID end function fibaro:getSectionID(deviceID) local dev = __fibaro_get_device(deviceID) if dev == nil then return nil end if dev.roomID ~= 0 then return HC2.getRoom(dev.roomID).sectionID end return 0 end function fibaro:getType(deviceID) YIELD() local dev = __fibaro_get_device(deviceID) if dev == nil then return nil end return dev.type end function fibaro:get(deviceID ,propertyName) --YIELD() local property = __fibaro_get_device_property(deviceID , propertyName) if property == nil then return nil end return __convertToString(property.value) , property.modified end function fibaro:getValue(deviceID ,propertyName) --YIELD() local property = __fibaro_get_device_property(deviceID , propertyName) return property and __convertToString(property.value) end function fibaro:getModificationTime(deviceID ,propertyName) local property = __fibaro_get_device_property(deviceID , propertyName) return property and property.modified end function fibaro:getGlobal(varName) YIELD(10) local globalVar = __fibaro_get_global_variable(varName) if globalVar == nil then return nil end return globalVar.value ,globalVar.modified end function fibaro:getGlobalValue(varName) YIELD(10) local globalVar = __fibaro_get_global_variable(varName) return globalVar and globalVar.value end function fibaro:getGlobalModificationTime(varName) return select(2,fibaro:getGlobal(varName)) end function fibaro:setGlobal(varName ,value) YIELD(10) __assert_type(varName ,"string") __fibaro_set_global_variable(varName,{value=tostring(value), invokeScenes= true}) end function fibaro:calculateDistance(position1 , position2) __assert_type(position1 ,"string") __assert_type(position2 ,"string") local lat1,lon1=position1:match("(.*);(.*)") local lat2,lon2=position2:match("(.*);(.*)") lat1,lon1,lat2,lon2=tonumber(lat1),tonumber(lon1),tonumber(lat2),tonumber(lon2) _assert(lat1 and lon1 and lat2 and lon2,"Bad arguments to fibaro:calculateDistance") local dlat = math.rad(lat2-lat1) local dlon = math.rad(lon2-lon1) local sin_dlat = math.sin(dlat/2) local sin_dlon = math.sin(dlon/2) local a = sin_dlat * sin_dlat + math.cos(math.rad(lat1)) * math.cos(math.rad(lat2)) * sin_dlon * sin_dlon local c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) local d = 6378 * c return d end function setAndPropagate(id,key,value) local d = HC2.rsrc.devices[id].properties if d[key] ~= value then d[key]=value HC2.rsrc.devices[id].modified=osTime() Event.post({type='property', deviceID=id, propertyName=key}) end end local _specCalls={} function _specCalls.setProperty(id,prop,...) setAndPropagate(id,prop,({...})[1]) end function _specCalls.setColor(id,R,G,B) setAndPropagate(id,"color","RGB") end function _specCalls.setArmed(id,value) setAndPropagate(id,"armed",value) end function _specCalls.sendPush(id,msg) end -- log to console? function _specCalls.pressButton(id,msg) end -- simulate VD? function _specCalls.setPower(id,value) setAndPropagate(id,"power",value) end function _specCalls.close(id,value) setAndPropagate(id,"value",0) end function _specCalls.open(id,value) setAndPropagate(id,"value",100) end function fibaro:call(deviceID ,actionName ,...) YIELD(10) deviceID = tonumber(deviceID) __assert_type(actionName ,"string") local dev = __fibaro_get_device(deviceID,true) if dev and dev._local then if _specCalls[actionName] then _specCalls[actionName](deviceID,...) return end local value = ({turnOff=false,turnOn=true,open=true, on=true,close=false, off=false})[actionName] or (actionName=='setValue' and tostring(({...})[1])) if value==nil then error(_format("fibaro:call(..,'%s',..) is not supported, fix it!",actionName)) end setAndPropagate(deviceID,'value',value) elseif dev then local args = "" for i,v in ipairs ({...}) do args = args.. '&arg'..tostring(i)..'='..urlencode(tostring(v)) end api.rawGet(true,"/callAction?deviceID="..deviceID.."&name="..actionName..args) end end function fibaro:getName(deviceID) __assert_type(deviceID ,'number') local dev = __fibaro_get_device(deviceID) return dev and dev.name end function fibaro:getRoomName(roomID) __assert_type(roomID ,'number') local room = __fibaro_get_room(roomID) return room and room.name end function fibaro:getRoomNameByDeviceID(deviceID) __assert_type(deviceID,'number') local dev = __fibaro_get_device(deviceID) if dev == nil then return nil end local room = __fibaro_get_room(dev.roomID) return dev.roomID==0 and 'unassigned' or room and room.name end function fibaro:wakeUpDeadDevice(deviceID ) __assert_type(deviceID ,'number') fibaro:call(1,'wakeUpDeadDevice',deviceID) end --[[ Expected input: { name: value, //: require name to be equal to value properties: { //: volume: "nil", //: require property volume to exist, any value ip: "127.0.0.1" //: require property ip to equal 127.0.0.1 }, interface: ifname //: require device to have interface ifname } ]]-- function fibaro:getDevicesId(filter) if type(filter) ~= 'table' or (type(filter) == 'table' and next(filter) == nil) then return fibaro:getIds(fibaro:getAllDeviceIds()) end local args = '/?' for c, d in pairs(filter) do if c == 'properties' and d ~= nil and type(d) == 'table' then for a, b in pairs(d) do if b == "nil" then args = args .. 'property=' .. tostring(a) .. '&' else args = args .. 'property=[' .. tostring(a) .. ',' .. tostring(b) .. ']&' end end elseif c == 'interfaces' and d ~= nil and type(d) == 'table' then for a, b in pairs(d) do args = args .. 'interface=' .. tostring(b) .. '&' end else args = args .. tostring(c) .. "=" .. tostring(d) .. '&' end end args = string.sub(args, 1, -2) return fibaro:getIds(api.get('/devices'..args)) end function fibaro:getAllDeviceIds() return HC2.getAllRsrc("devices") end function fibaro:getIds(devices) local ids = {} for _,a in pairs(devices) do if a ~= nil and type(a) == 'table' and a['id'] ~= nil and a['id'] > 3 then table.insert(ids, a['id']) end end return ids end function urlencode(str) if str then str = str:gsub("\n", "\r\n") str = str:gsub("([^%w %-%_%.%~])", function(c) return ("%%%02X"):format(string.byte(c)) end) str = str:gsub(" ", "%%20") end return str end function urldecode(str) return str:gsub('%%(%x%x)',function (x) return string.char(tonumber(x,16)) end) end function split(s, sep) local fields = {} sep = sep or " " local pattern = string.format("([^%s]+)", sep) string.gsub(s, pattern, function(c) fields[#fields + 1] = c end) return fields end net = {} -- An emulation of Fibaro's net.HTTPClient local _HTTP = {} -- It is synchronous, but synchronous is a speciell case of asynchronous.. :-) function net.HTTPClient() return _HTTP end -- Not sure I got all the options right.. function _HTTP:request(url,options) local resp = {} options = options or {} local req = options.options or {} req.url = url req.headers = req.headers or {} req.sink = ltn12.sink.table(resp) if req.data then req.headers["Content-Length"] = #req.data req.source = ltn12.source.string(req.data) end local response, status, headers, timeout http.TIMEOUT,timeout=req.timeout and math.floor(req.timeout/1000) or http.TIMEOUT, http.TIMEOUT if url:lower():match("^https") then response, status, headers = https.request(req) else response, status, headers = http.request(req) end http.TIMEOUT = timeout local delay = math.random(1,3000) if response == 1 then if options.success then Runtime.setTimeout(function() options.success({status=status, headers=headers, data=table.concat(resp)}) end,delay) end else if options.error then Runtime.setTimeout(function() options.error(status) end, delay) end end end api={} -- Emulation of api.get/put/post local function rawCall(dbg,method,call,data,cType) if dbg and _debugFlags.hc2calls then Log(LOG.LOG,"HC2 call:%s:%s",method,call) end local resp = {} local req={ method=method, timeout=5000, url = "http://".._HC2_IP.."/api"..call,sink = ltn12.sink.table(resp), user=_HC2_USER, password=_HC2_PWD, headers={} } if data then req.headers["Content-Type"] = cType req.headers["Content-Length"] = #data req.source = ltn12.source.string(data) end local r, c = http.request(req) if not r then Log(LOG.ERROR,"Error connnecting to HC2: '%s' - URL: '%s'.",c,req.url) os.exit(1) end if c>=200 and c<300 then return resp[1] and json.decode(table.concat(resp)) or nil end Log(LOG.ERROR,"HC2 returned error '%d %s' - URL: '%s'.",c,resp[1] or "",req.url) os.exit(1) end function api.get(call) return HC2.apiCall("GET",call) end function api.put(call, data) return HC2.apiCall("PUT",call,data,"application/json") end function api.post(call, data) return HC2.apiCall("POST",call,data,"application/json") end function api.delete(call, data) return HC2.apiCall("DELETE",call,data,"application/json") end function api.rawGet(l,call) return rawCall(l,"GET",call) end function api.rawPut(l,call, data) return rawCall(l,"PUT",call,json.encode(data),"application/json") end function api.rawPost(l,call, data) return rawCall(l,"POST",call,json.encode(data),"application/json") end function api.rawDelete(l,call, data) return rawCall(l,"DELETE",call,json.encode(data),"application/json") end HomeCenter = { PopupService = { publish = function(request) local response = api.post('/popups', request) return response end }, SystemService = { reboot = function() Log(LOG.LOG,"SystemService.reboot() -- ignoring") end, shutdown = function() Log(LOG.LOG,"SystemService.shutdown() -- ignoring") end } } -------- Fibaro log support -------------- -- Logging of fibaro:* calls ------------- fibaro.mapTable = {} local function reverseMap(path,value) if type(value) == 'number' then fibaro.mapTable[tostring(value)] = table.concat(path,".") elseif type(value) == 'table' and not value[1] then for k,v in pairs(value) do table.insert(path,k) reverseMap(path,v) table.remove(path) end end end local function reverseVar(id) return fibaro.mapTable[tostring(id)] or id end function traceFibaroIDs(tb) _assert(type(tb) == 'table',"traceFibaroIDs bad argument") reverseMap({},tb) end if _System then _System.reverseMapDef = traceFibaroIDs end local function checkFlag(flag,def) local env = Scene.global() if env and env._debugFlags then return env._debugFlags[flag] else return _debugFlags[flag] end end function traceFibaro(name,flag,rt) local orgFun=fibaro[name] fibaro[name]=function(f,id,...) --if id then id=reverseVar(id) end local args={...} local stat,res = pcall(function() return {orgFun(f,id,table.unpack(args))} end) if stat then if checkFlag(flag) then if rt then rt(id,args,res) else local astr=(id~=nil and reverseVar(id).."," or "")..json.encode(args):sub(2,-2) Debug(true,"fibaro:%s(%s)%s",name,astr,#res>0 and "="..tojson(res):sub(2,-2) or "") end end if #res>0 then return table.unpack(res) else return nil end else local astr=(id~=nil and reverseVar(id).."," or "")..json.encode(args):sub(2,-2) error(_format("fibaro:%s(%s),%s",name,astr,res),3) end end end function fibaro._logFibaroCalls() for _,f in ipairs({ {"call","fcall"}, {"setGlobal","fglobal"}, {"getGlobal","fglobal"}, {"getGlobalValue","fglobal"}, {"get","fget"}, {"getValue","fget"}, {"killScenes","fother"}, {"abort","fother"}, {"sleep","fother",function(id,args,res) Debug(true,"fibaro:sleep(%s) until %s",id,osDate("%X",osTime()+math.floor(0.5+id/1000))) end}, {"startScene","fother",function(id,args,res) local a = Util.isRemoteEvent(args[1]) and json.encode(Util.decodeRemoteEvent(args[1 ])) or args and json.encode(args) Debug(true,"fibaro:startScene(%s%s)",id,a and ","..a or "") end}, }) do traceFibaro(f[1],f[2],f[3]) end end end -------------------------------------- -- EventRunner support -------------------------------------- function ER_functions() ER={ gEventRunnerKey="6w8562395ue734r437fg3" } function ER.checkForEventRunner(scene) scene.EventRunner = scene.lua:match(ER.gEventRunnerKey) if scene.EventRunner then scene._startMsg = function(id,inst,env) return inst > 0 end scene._terminateMsg = function(id,inst,env) return inst > 1 end end end function ER.announceEmulator(ipaddress,port) -- Tell HC2 what local scenes we have. local locals,remotes={},{} for id,scene in pairs(HC2.rsrc.scenes) do if scene._local then locals[#locals+1]=id elseif scene.EventRunner then -- Should we check that they are active too? remotes[#remotes+1]=id end end if #remotes>0 then local event = {type='%%EMU%%',ids=locals,adress="http://"..ipaddress..":"..port.."/"} local args=Util.encodeRemoteEvent(event) for _,sceneID in ipairs(remotes) do api.rawPost(true,"/scenes/"..sceneID.."/action/start",{args=args}) end end end end ------------------------------------------------------------------------------- -- Libs, json etc --------------------------------------------------------------------------------- function libs() local lunpack = table.unpack table.unpack = function(t) return lunpack(t,1,table.maxn(t)) end table.pack = function(...) return { n = select("#", ...), ... } end if not _VERSION:match("5%.1") then loadstring = load function setfenv(fn, env) local i = 1 while true do local name = debug.getupvalue(fn, i) if name == "_ENV" then debug.upvaluejoin(fn, i, (function() return env end), 1) break elseif not name then break end i = i + 1 end return fn end function getfenv(fn) local i = 1 while true do local name, val = debug.getupvalue(fn, i) if name == "_ENV" then return val elseif not name then break end i = i + 1 end end end -------------------------------------------------------------------------------- -- json support -- json library - Copyright (c) 2018 rxi https://github.com/rxi/json.lua ----------------------------------------------------------------------------- json = { _version = "0.1.1" } ------------------------------------------------------------------------------- -- Encode ------------------------------------------------------------------------------- do local encode local escape_char_map = { [ "\\" ] = "\\\\", [ "\"" ] = "\\\"", [ "\b" ] = "\\b", [ "\f" ] = "\\f", [ "\n" ] = "\\n", [ "\r" ] = "\\r", [ "\t" ] = "\\t", } local escape_char_map_inv = { [ "\\/" ] = "/" } for k, v in pairs(escape_char_map) do escape_char_map_inv[v] = k end local function escape_char(c) return escape_char_map[c] or string.format("\\u%04x", c:byte()) end local function encode_nil(val) return "null" end local function encode_table(val, stack) local res = {} stack = stack or {} -- Circular reference? if stack[val] then error("circular reference") end stack[val] = true if val[1] ~= nil or next(val) == nil then -- Treat as array -- check keys are valid and it is not sparse local n = 0 for k in pairs(val) do if type(k) ~= "number" then error("invalid table: mixed or invalid key types") end n = n + 1 end if n ~= #val then error("invalid table: sparse array") end -- Encode for i, v in ipairs(val) do table.insert(res, encode(v, stack)) end stack[val] = nil return "[" .. table.concat(res, ",") .. "]" else -- Treat as an object for k, v in pairs(val) do if type(k) ~= "string" then error("invalid table: mixed or invalid key types") end table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) end stack[val] = nil return "{" .. table.concat(res, ",") .. "}" end end local function encode_string(val) return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' end local function encode_number(val) -- Check for NaN, -inf and inf if val ~= val or val <= -math.huge or val >= math.huge then error("unexpected number value '" .. tostring(val) .. "'") end return string.format("%.14g", val) end local type_func_map = { [ "nil" ] = encode_nil, [ "table" ] = encode_table, [ "string" ] = encode_string, [ "number" ] = encode_number, [ "boolean" ] = tostring, } encode = function(val, stack) local t = type(val) local f = type_func_map[t] if f then return f(val, stack) end error("unexpected type '" .. t .. "'") end function json.encode(val) return ( encode(val) ) end ------------------------------------------------------------------------------- -- Decode ------------------------------------------------------------------------------- local parse local function create_set(...) local res = {} for i = 1, select("#", ...) do res[ select(i, ...) ] = true end return res end local space_chars = create_set(" ", "\t", "\r", "\n") local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") local literals = create_set("true", "false", "null") local literal_map = { [ "true" ] = true, [ "false" ] = false, [ "null" ] = nil, } local function next_char(str, idx, set, negate) for i = idx, #str do if set[str:sub(i, i)] ~= negate then return i end end return #str + 1 end local function decode_error(str, idx, msg) local line_count = 1 local col_count = 1 for i = 1, idx - 1 do col_count = col_count + 1 if str:sub(i, i) == "\n" then line_count = line_count + 1 col_count = 1 end end error( string.format("%s at line %d col %d", msg, line_count, col_count) ) end local function codepoint_to_utf8(n) -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa local f = math.floor if n <= 0x7f then return string.char(n) elseif n <= 0x7ff then return string.char(f(n / 64) + 192, n % 64 + 128) elseif n <= 0xffff then return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) elseif n <= 0x10ffff then return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, f(n % 4096 / 64) + 128, n % 64 + 128) end error( string.format("invalid unicode codepoint '%x'", n) ) end local function parse_unicode_escape(s) local n1 = tonumber( s:sub(3, 6), 16 ) local n2 = tonumber( s:sub(9, 12), 16 ) -- Surrogate pair? if n2 then return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) else return codepoint_to_utf8(n1) end end local function parse_string(str, i) local has_unicode_escape = false local has_surrogate_escape = false local has_escape = false local last for j = i + 1, #str do local x = str:byte(j) if x < 32 then decode_error(str, j, "control character in string") end if last == 92 then -- "\\" (escape char) if x == 117 then -- "u" (unicode escape sequence) local hex = str:sub(j + 1, j + 5) if not hex:find("%x%x%x%x") then decode_error(str, j, "invalid unicode escape in string") end if hex:find("^[dD][89aAbB]") then has_surrogate_escape = true else has_unicode_escape = true end else local c = string.char(x) if not escape_chars[c] then decode_error(str, j, "invalid escape char '" .. c .. "' in string") end has_escape = true end last = nil elseif x == 34 then -- '"' (end of string) local s = str:sub(i + 1, j - 1) if has_surrogate_escape then s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) end if has_unicode_escape then s = s:gsub("\\u....", parse_unicode_escape) end if has_escape then s = s:gsub("\\.", escape_char_map_inv) end return s, j + 1 else last = x end end decode_error(str, i, "expected closing quote for string") end local function parse_number(str, i) local x = next_char(str, i, delim_chars) local s = str:sub(i, x - 1) local n = tonumber(s) if not n then decode_error(str, i, "invalid number '" .. s .. "'") end return n, x end local function parse_literal(str, i) local x = next_char(str, i, delim_chars) local word = str:sub(i, x - 1) if not literals[word] then decode_error(str, i, "invalid literal '" .. word .. "'") end return literal_map[word], x end local function parse_array(str, i) local res = {} local n = 1 i = i + 1 while 1 do local x i = next_char(str, i, space_chars, true) -- Empty / end of array? if str:sub(i, i) == "]" then i = i + 1 break end -- Read token x, i = parse(str, i) res[n] = x n = n + 1 -- Next token i = next_char(str, i, space_chars, true) local chr = str:sub(i, i) i = i + 1 if chr == "]" then break end if chr ~= "," then decode_error(str, i, "expected ']' or ','") end end return res, i end local function parse_object(str, i) local res = {} i = i + 1 while 1 do local key, val i = next_char(str, i, space_chars, true) -- Empty / end of object? if str:sub(i, i) == "}" then i = i + 1 break end -- Read key if str:sub(i, i) ~= '"' then decode_error(str, i, "expected string for key") end key, i = parse(str, i) -- Read ':' delimiter i = next_char(str, i, space_chars, true) if str:sub(i, i) ~= ":" then decode_error(str, i, "expected ':' after key") end i = next_char(str, i + 1, space_chars, true) -- Read value val, i = parse(str, i) -- Set res[key] = val -- Next token i = next_char(str, i, space_chars, true) local chr = str:sub(i, i) i = i + 1 if chr == "}" then break end if chr ~= "," then decode_error(str, i, "expected '}' or ','") end end return res, i end local char_func_map = { [ '"' ] = parse_string, [ "0" ] = parse_number, [ "1" ] = parse_number, [ "2" ] = parse_number, [ "3" ] = parse_number, [ "4" ] = parse_number, [ "5" ] = parse_number, [ "6" ] = parse_number, [ "7" ] = parse_number, [ "8" ] = parse_number, [ "9" ] = parse_number, [ "-" ] = parse_number, [ "t" ] = parse_literal, [ "f" ] = parse_literal, [ "n" ] = parse_literal, [ "[" ] = parse_array, [ "{" ] = parse_object, } parse = function(str, idx) local chr = str:sub(idx, idx) local f = char_func_map[chr] if f then return f(str, idx) end decode_error(str, idx, "unexpected character '" .. chr .. "'") end function json.decode(str) if type(str) ~= "string" then error("expected argument of type string, got " .. type(str),2) end local stat,res = pcall(function() local res, idx = parse(str, next_char(str, 1, space_chars, true)) idx = next_char(str, idx, space_chars, true) if idx <= #str then decode_error(str, idx, "trailing garbage") end return res end) if not stat then error(res,2) else return res end end end tojson = json.encode Util = Util or {} Util.gKeys = {type=1,deviceID=2,value=3,val=4,key=5,arg=6,event=7,events=8,msg=9,res=10} Util.gKeysNext = 10 function Util._keyCompare(a,b) local av,bv = Util.gKeys[a], Util.gKeys[b] if av == nil then Util.gKeysNext = Util.gKeysNext+1 Util.gKeys[a] = Util.gKeysNext av = Util.gKeysNext end if bv == nil then Util.gKeysNext = Util.gKeysNext+1 Util.gKeys[b] = Util.gKeysNext bv = Util.gKeysNext end return av < bv end function Util.prettyJson(e) -- our own json encode, as we don't have 'pure' json structs, and sorts keys in order local res,seen,t = {},{} local function pretty(e) local t = type(e) if t == 'string' then res[#res+1] = '"' res[#res+1] = e res[#res+1] = '"' elseif t == 'number' then res[#res+1] = e elseif t == 'boolean' or t == 'function' or t=='thread' then res[#res+1] = tostring(e) elseif t == 'table' then if next(e)==nil then res[#res+1]='{}' elseif seen[e] then res[#res+1]="..rec.." elseif e[1] or #e>0 then seen[e]=true res[#res+1] = "[" pretty(e[1]) for i=2,#e do res[#res+1] = "," pretty(e[i]) end res[#res+1] = "]" else seen[e]=true if e._var_ then res[#res+1] = _format('"%s"',e._str) return end local k = {} for key,_ in pairs(e) do k[#k+1] = key end table.sort(k,Util._keyCompare) if #k == 0 then res[#res+1] = "[]" return end res[#res+1] = '{'; res[#res+1] = '"' res[#res+1] = k[1]; res[#res+1] = '":' t = k[1] pretty(e[t]) for i=2,#k do res[#res+1] = ',"' res[#res+1] = k[i]; res[#res+1] = '":' t = k[i] pretty(e[t]) end res[#res+1] = '}' end elseif e == nil then res[#res+1]='null' else error("bad json expr:"..tostring(e)) end end pretty(e) return table.concat(res) end tojson = Util.prettyJson ----------------------------- -- persistence -- Copyright (c) 2010 Gerhard Roethlin -------------------- -- Private methods do local write, writeIndent, writers, refCount; persistence = { store = function (path, ...) local file, e; if type(path) == "string" then -- Path, open a file file, e = io.open(path, "w"); if not file then return error(e); end else -- Just treat it as file file = path; end local n = select("#", ...); -- Count references local objRefCount = {}; -- Stores reference that will be exported for i = 1, n do refCount(objRefCount, (select(i,...))); end; -- Export Objects with more than one ref and assign name -- First, create empty tables for each local objRefNames = {}; local objRefIdx = 0; file:write("-- Persistent Data\n"); file:write("local multiRefObjects = {\n"); for obj, count in pairs(objRefCount) do if count > 1 then objRefIdx = objRefIdx + 1; objRefNames[obj] = objRefIdx; file:write("{};"); -- table objRefIdx end; end; file:write("\n} -- multiRefObjects\n"); -- Then fill them (this requires all empty multiRefObjects to exist) for obj, idx in pairs(objRefNames) do for k, v in pairs(obj) do file:write("multiRefObjects["..idx.."]["); write(file, k, 0, objRefNames); file:write("] = "); write(file, v, 0, objRefNames); file:write(";\n"); end; end; -- Create the remaining objects for i = 1, n do file:write("local ".."obj"..i.." = "); write(file, (select(i,...)), 0, objRefNames); file:write("\n"); end -- Return them if n > 0 then file:write("return obj1"); for i = 2, n do file:write(" ,obj"..i); end; file:write("\n"); else file:write("return\n"); end; file:close(); end; load = function (path) local f, e = loadfile(path); if f then return f(); else return nil, e; end; end; } -- Private methods -- write thing (dispatcher) write = function (file, item, level, objRefNames) writers[type(item)](file, item, level, objRefNames); end; -- write indent writeIndent = function (file, level) for i = 1, level do file:write("\t"); end; end; -- recursively count references refCount = function (objRefCount, item) -- only count reference types (tables) if type(item) == "table" then -- Increase ref count if objRefCount[item] then objRefCount[item] = objRefCount[item] + 1; else objRefCount[item] = 1; -- If first encounter, traverse for k, v in pairs(item) do refCount(objRefCount, k); refCount(objRefCount, v); end; end; end; end; -- Format items for the purpose of restoring writers = { ["nil"] = function (file, item) file:write("nil"); end; ["number"] = function (file, item) file:write(tostring(item)); end; ["string"] = function (file, item) file:write(string.format("%q", item)); end; ["boolean"] = function (file, item) if item then file:write("true"); else file:write("false"); end end; ["table"] = function (file, item, level, objRefNames) local refIdx = objRefNames[item]; if refIdx then -- Table with multiple references file:write("multiRefObjects["..refIdx.."]"); else -- Single use table file:write("{\n"); for k, v in pairs(item) do writeIndent(file, level+1); file:write("["); write(file, k, level+1, objRefNames); file:write("] = "); write(file, v, level+1, objRefNames); file:write(";\n"); end writeIndent(file, level); file:write("}"); end; end; ["function"] = function (file, item) -- Does only work for "normal" functions, not those -- with upvalues or c functions local dInfo = debug.getinfo(item, "uS"); if dInfo.nups > 0 then file:write("nil --[[functions with upvalue not supported]]"); elseif dInfo.what ~= "Lua" then file:write("nil --[[non-lua function not supported]]"); else local r, s = pcall(string.dump,item); if r then file:write(string.format("loadstring(%q)", s)); else file:write("nil --[[function could not be dumped]]"); end end end; ["thread"] = function (file, item) file:write("nil --[[thread]]\n"); end; ["userdata"] = function (file, item) file:write("nil --[[userdata]]\n"); end; } end ------------------- Sunset/Sunrise --------------- -- \fibaro\usr\share\lua\5.2\common\lustrous.lua based on the United States Naval Observatory SunCalc={} function SunCalc.sunturnTime(date, rising, latitude, longitude, zenith, local_offset) local rad,deg,floor = math.rad,math.deg,math.floor local frac = function(n) return n - floor(n) end local cos = function(d) return math.cos(rad(d)) end local acos = function(d) return deg(math.acos(d)) end local sin = function(d) return math.sin(rad(d)) end local asin = function(d) return deg(math.asin(d)) end local tan = function(d) return math.tan(rad(d)) end local atan = function(d) return deg(math.atan(d)) end local function day_of_year(date) local n1 = floor(275 * date.month / 9) local n2 = floor((date.month + 9) / 12) local n3 = (1 + floor((date.year - 4 * floor(date.year / 4) + 2) / 3)) return n1 - (n2 * n3) + date.day - 30 end local function fit_into_range(val, min, max) local range,count = max - min if val < min then count = floor((min - val) / range) + 1; return val + count * range elseif val >= max then count = floor((val - max) / range) + 1; return val - count * range else return val end end -- Convert the longitude to hour value and calculate an approximate time local n,lng_hour,t = day_of_year(date), longitude / 15, nil if rising then t = n + ((6 - lng_hour) / 24) -- Rising time is desired else t = n + ((18 - lng_hour) / 24) end -- Setting time is desired local M = (0.9856 * t) - 3.289 -- Calculate the Sun^s mean anomaly -- Calculate the Sun^s true longitude local L = fit_into_range(M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634, 0, 360) -- Calculate the Sun^s right ascension local RA = fit_into_range(atan(0.91764 * tan(L)), 0, 360) -- Right ascension value needs to be in the same quadrant as L local Lquadrant = floor(L / 90) * 90 local RAquadrant = floor(RA / 90) * 90 RA = RA + Lquadrant - RAquadrant; RA = RA / 15 -- Right ascension value needs to be converted into hours local sinDec = 0.39782 * sin(L) -- Calculate the Sun's declination local cosDec = cos(asin(sinDec)) local cosH = (cos(zenith) - (sinDec * sin(latitude))) / (cosDec * cos(latitude)) -- Calculate the Sun^s local hour angle if rising and cosH > 1 then return "N/R" -- The sun never rises on this location on the specified date elseif cosH < -1 then return "N/S" end -- The sun never sets on this location on the specified date local H -- Finish calculating H and convert into hours if rising then H = 360 - acos(cosH) else H = acos(cosH) end H = H / 15 local T = H + RA - (0.06571 * t) - 6.622 -- Calculate local mean time of rising/setting local UT = fit_into_range(T - lng_hour, 0, 24) -- Adjust back to UTC local LT = UT + local_offset -- Convert UT value to local time zone of latitude/longitude return osTime({day = date.day,month = date.month,year = date.year,hour = floor(LT),min = math.modf(frac(LT) * 60)}) end function SunCalc.getTimezone() local now = osTime() return os.difftime(now, osTime(osDate("!*t", now))) end function SunCalc.sunCalc(time) local hc2Info = HC2 and HC2.getLocation() or {} local lat = hc2Info.latitude or _LATITUDE local lon = hc2Info.longitude or _LONGITUDE local utc = SunCalc.getTimezone() / 3600 local zenith,zenith_twilight = 90.83, 96.0 -- sunset/sunrise 90°50′, civil twilight 96°0′ local date = osDate("*t",time or osTime()) if date.isdst then utc = utc + 1 end local rise_time = osDate("*t", SunCalc.sunturnTime(date, true, lat, lon, zenith, utc)) local set_time = osDate("*t", SunCalc.sunturnTime(date, false, lat, lon, zenith, utc)) local rise_time_t = osDate("*t", SunCalc.sunturnTime(date, true, lat, lon, zenith_twilight, utc)) local set_time_t = osDate("*t", SunCalc.sunturnTime(date, false, lat, lon, zenith_twilight, utc)) local sunrise = _format("%.2d:%.2d", rise_time.hour, rise_time.min) local sunset = _format("%.2d:%.2d", set_time.hour, set_time.min) local sunrise_t = _format("%.2d:%.2d", rise_time_t.hour, rise_time_t.min) local sunset_t = _format("%.2d:%.2d", set_time_t.hour, set_time_t.min) return sunrise, sunset, sunrise_t, sunset_t end end --------------------------------------------------------------------------------- ---- Web GUI pages -------------------------------------------------------------------------------- function pages() local P_MAIN = [[HTTP/1.1 200 OK Content-Type: text/html Cache-Control: no-cache, no-store, must-revalidate <<>> <<<return _format("%s v%s%s",_sceneName,_version,_fix~="" and " ,".._fix or "")>>>
Emulator Devices Scenes Globals
]] local P_EMU = [[HTTP/1.1 200 OK Content-Type: text/html Cache-Control: no-cache, no-store, must-revalidate <<>> <<<return _format("%s v%s%s",_sceneName,_version,_fix~="" and " ,".._fix or "")>>>

HC2 Emulator <<>>

<<Globals:%s
Devices:%s
Rooms:%s
",c.scenes,c.glob,c.dev,c.rooms)>>>
>>" class="button" style="background-color: #FFA500;">Copy setup from HC2 Creates HC2.data file. Please restart emulator afterward.

>>" class="button" style="background-color: #FFA500;">Speed>> >>" class="button" style="background-color: #FFA500;">1 hour>> >>" class="button" style="background-color: #FFA500;">24 hours>> Speed emulator.

>>" class="button" style="background-color: #FFA500;">Install trigger proxy Creates a scene on the HC2 that forwards triggers to the emulator, triggers listed below. Will only install for trigger marked 'remote' [R].

>>" class="button" style="background-color: #FFA500;">Remove trigger proxy Remove trigger proxy if installed.

<<< local ts,res=Scene.getAllLoadedTriggers(),{""} for _,t in ipairs(ts) do if t.type=='property' then local id = t.deviceID res[#res+1]=_format("" elseif t.type=='global' then res[#res+1]=_format("" elseif t.type=='event' then local id = Util.getIDfromTrigger[t.type](t) res[#res+1]= _format("",Pages.lr("devices",id),id,t.event.type) end end res[#res+1]="
[%s]DeviceID:%s ",Pages.lr("devices",id),id)..Pages.renderAction(id,"call","setValue","setValue","").."
[%s]Global:'%s' ",Pages.lr("globalVariables",t.name),t.name)..Pages.renderAction(t.name,"setGlobal","","setValue","").."
[%s]DeviceID:%s %s
" return table.concat(res) >>>

]] local P_SCENES = [[HTTP/1.1 200 OK Content-Type: text/html Cache-Control: no-cache, no-store, must-revalidate <<>> Scenes <<< local res={} for id,dev in pairs(HC2.rsrc.scenes) do local function actions(id,dev) local res={} res[#res+1]=Pages.renderStopScene(id) res[#res+1]=Pages.renderStartScene(id) return "
"..table.concat(res).."
" end res[#res+1] = _format("", id,dev.name,dev.runningInstances,dev._local and "Local" or "Remote",actions(id) ) end return table.concat(res) >>>
sceneIDNameInstancesWhereActions
%s%s%s%s%s
]] local P_DEVICES = [[HTTP/1.1 200 OK Content-Type: text/html Triggers <<>> <<< local res={} for id,dev in pairs(HC2.rsrc.devices) do if id > 3 and dev.type~="virtual_device" then local function actions(id,dev) local res={} local val = dev.properties.value val = val ~= nil and tostring(val) or false res[#res+1]=Pages.renderAction(id,"call","turnOn","turnOn",false) res[#res+1]=Pages.renderAction(id,"call","turnOff","turnOff",false) if val then res[#res+1]=Pages.renderAction(id,"call","setValue","setValue",val) end return "
"..table.concat(res).."
" end res[#res+1] = _format("", id,dev.name,dev.type,dev.properties.value,dev._local and "Local" or "Remote",actions(id,dev)) end end return table.concat(res) >>>
deviceIDNameTypeValueWhereActions
%s%s%s%s%s%s
]] local P_GLOBALS = [[HTTP/1.1 200 OK Content-Type: text/html Globals <<>> <<< local res={} for name,gl in pairs(HC2.rsrc.globalVariables) do res[#res+1] = _format("", gl.name,gl.value,gl._local and "Local" or "Remote") end return table.concat(res) >>>
NameValueWhere
%s%s%s
]] local P_ERROR1 = [[HTTP/1.1 200 OK Content-Type: text/html Cache-Control: no-cache, no-store, must-revalidate Error
%s
]] Pages = { pages={} } function Pages.lr(t,id) local r = HC2.getRsrc(t,id,true) return r and r._local and "L" or "R" end function Pages.register(path,page) local file = page:match("^file:(.*)") if file then local f = io.open(file) if not f then error("No such file:"..file) end local page = f:read("*all") end Pages.pages[path]={page=page, path=path} return Pages.pages[path] end function Pages.getPath(path) local p = Pages.pages[path] if p and not p.cpage then Pages.compile(p) end if p then return Pages.render(p) else return null end end function Pages.renderError(msg) return _format(P_ERROR1,msg) end function Pages.render(p) if p.static and p.static~=true then return p.static end local stat,res = pcall(function() return p.cpage:gsub("<<<(%d+)>>>", function(i) return p.funs[tonumber(i)]() end) end) if not stat then return Pages.renderError(res) else p.static=res return res end end function Pages.renderAction(id,method,action1,action2,value) local res if not value then res = _format([[
]],method,id,action1,action2) else res = _format([[
]],method,id,action1,action2,value) end return res end function Pages.renderStopScene(id) return _format([[
]],id) end function Pages.renderStartScene(id) return _format([[
]],id) end function Pages.compile(p) local funs={} p.cpage=p.page:gsub("<<<(.-)>>>", function(code) local f = _format("do %s end",code) f,m = loadstring(f) if m then printf("ERROR RENDERING PAGE %s, %s",p.path,m) end funs[#funs+1]=f return (_format("<<<%s>>>",#funs)) end) p.funs=funs end Pages.register("/emu/main",P_MAIN).static=true Pages.register("/emu/emu",P_EMU) Pages.register("/emu/scenes",P_SCENES) Pages.register("/emu/devices",P_DEVICES) Pages.register("/emu/globals",P_GLOBALS) _PAGE_STYLE= [[ ]] end -------------------------------------------------- -- Load code and start -------------------------------------------------- if _EMULATED and type(_EMULATED)=='table' then -- Setup parameters from calling scene local d = _EMULATED if type(d.ip)=='string' then _HC2_IP=d.ip end if type(d.user)=='string' then _HC2_USER=d.user end if type(d.pwd)=='string' then _HC2_PWD=d.pwd end if type(d.name)=='string' then _SCENE_NAME=d.name end if type(d.id)=='number' then _SCENE_ID=d.id end if d.loc~=nil then _LOCAL=d.loc end if d.speed~=nil then _SPEEDTIME=d.speed end if type(d.maxtime)=='number' then _MAXTIME=d.maxtime end if d.color~=nil then _COLOR=d.color end if type(d.data)=='string' then _HC2_FILE=d.data end if d.force~=nil then _FORCERESOURCEUPDATE=d.force end if d.logapi~=nil then _DEBUGREMOTERSRC=d.logapi end if d.autodevice~=nil then _AUTOCREATEGLOBALS=d.autodevice end if d.autoglobal~=nil then _AUTOCREATEGLOBALS=d.autoglobal end if d.blockput~=nil then _BLOCK_PUT=d.blockput end if d.blockpost~=nil then _BLOCK_POST=d.blockpost end end libs() pages() Support_functions() Web_functions() Proxy_functions() Scene_functions() HC2_functions() Runtime_functions() Event_functions() System_functions() Fibaro_functions() ER_functions() startup()