local _ = require('lodash') local GLOBAL_DATA_MODULE = 'global_data' local utils = require('Utils') local persistence = require('persistence') local HTTPResponse = require('HTTPResponse') local ShellCommandResponse = require('ShellCommandResponse') local Timer = require('Timer') local Security = require('Security') local SystemEvent = require('SystemEvent') local CustomEvent = require('CustomEvent') local HistoricalStorage = require('HistoricalStorage') local sep = string.sub(package.config, 1, 1) local function EventHelpers(domoticz, mainMethod) local _gv = globalvariables local globalsDefinition local currentPath = _gv['script_path'] if (_G.TESTMODE) then -- make sure you run the tests from the tests folder !!!! _G.scriptsFolderPath = currentPath .. 'scripts' package.path = currentPath .. 'scripts/?.lua;' .. currentPath .. 'data/?.lua;' .. currentPath .. '/../?.lua;' .. package.path end local validEventTypes = 'devices,timer,security,customEvents,system,httpResponses,shellCommandResponses,scenes,groups,variables' local inValidEventTypes = 'on,logging,active,data,execute' -- defaults local hyperTextTransferProtocol = 'http' local serverPort = '8080' local serverAddress = '127.0.0.1' if _gv.domoticz_listening_port and tostring(_gv.domoticz_listening_port) ~= '' and tonumber(_gv.domoticz_listening_port) ~= 0 then serverPort = _gv.domoticz_listening_port elseif _gv.domoticz_is_secure and _gv.domoticz_secure_listening_port and tostring(_gv.domoticz_secure_listening_port) ~= '' and tonumber(_gv.domoticz_secure_listening_port) ~= 0 then hyperTextTransferProtocol = 'https' serverPort = _gv.domoticz_secure_listening_port end if _gv.domoticz_wwwbind ~= nil and tostring(_gv.domoticz_wwwbind) ~= '' and tostring(_gv.domoticz_wwwbind) ~= '::' then serverAddress = _gv.domoticz_wwwbind end local API_url = hyperTextTransferProtocol .. '://' .. serverAddress .. ':' .. serverPort local settings = { ['Log level'] = _gv.dzVents_log_level or 1, ['Domoticz url'] = API_url, url = API_url, webRoot = _gv.domoticz_webroot, wwwBind = _gv.domoticz_wwwbind, serverPort = port, secureServer = _gv.domoticz_is_secure, dzVentsVersion = _gv.dzVents_version, domoticzVersion = _gv.domoticz_version, location = { name = _gv.domoticz_title or 'Domoticz', latitude = _gv.latitude or 0, longitude = _gv.longitude or 0, } } _G.logLevel = settings['Log level'] if settings.webRoot ~= nil and settings.webRoot ~= '' then settings['Domoticz url'] = settings['Domoticz url'] .. '/' .. settings.webRoot settings.url = settings['Domoticz url'] end if (domoticz == nil) then local Domoticz = require('Domoticz') domoticz = Domoticz(settings) end local self = { utils = utils, -- convenient for testing and stubbing domoticz = domoticz, settings = settings, } if (_G.TESTMODE) then self.scriptsFolderPath = scriptsFolderPath self.dataFolderPath = _G.dataFolderPath function self._getUtilsInstance() return utils end end function self.getStorageContext(storageDef, module) local storageContext = {} local fileStorage, value, ok if (storageDef ~= nil) then -- load the datafile for this module ok, fileStorage = pcall(require, module) package.loaded[module] = nil -- no caching if (ok) then -- only transfer data as defined in storageDef for _var, _def in pairs(storageDef) do local var, def if (type(_var) == 'number' and type(_def) == 'string') then var = _def def = {} else var = _var def = _def end if type(fileStorage) == 'boolean' then local function preserve(fullQualifiedName, fullQualifiedNameFaulty) local inf = io.open(fullQualifiedName, 'rb') local outf = io.open(fullQualifiedNameFaulty, 'w') outf:write(inf:read('*a')) inf:close() outf:close() os.remove(fullQualifiedName) end local fullQualifiedName = _G.dataFolderPath .. sep .. module .. '.lua' local fullQualifiedNameFaulty = _G.dataFolderPath .. sep .. module .. '.faulty' utils.log('There was an issue with the require of the datamodule "' .. fullQualifiedName .. '"', utils.LOG_ERROR) preserve(fullQualifiedName,fullQualifiedNameFaulty) elseif def.history ~= nil and def.history == true then storageContext[var] = HistoricalStorage(fileStorage[var], def.maxItems, def.maxHours, def.maxMinutes, def.getValue) else if (fileStorage[var] == nil) then -- obviously this is a var that was added later -- initialize it storageContext[var] = def.initial else storageContext[var] = fileStorage[var] end end end else for _var, _def in pairs(storageDef) do local var, def if (type(_var) == 'number' and type(_def) == 'string') then var = _def def = {} else var = _var def = _def end if (def.history ~= nil and def.history == true) then -- no initial value, just an empty history storageContext[var] = HistoricalStorage(fileStorage[var], def.maxItems, def.maxHours, def.maxMinutes, def.getValue) else if (def.initial ~= nil) then storageContext[var] = def.initial else storageContext[var] = nil end end end end end storageContext.initialize = function(varName) if (storageContext[varName] ~= nil and storageDef[varName].history == nil ) then storageContext[varName] = storageDef[varName].initial end end fileStorage = nil return storageContext end function self.writeStorageContext(storageDef, dataFilePath, dataFileModuleName, storageContext) local data = {} if (storageDef ~= nil) then -- transfer only stuf as described in storageDef for var, def in pairs(storageDef) do if (type(var) == 'number' and type(def) == 'string') then data[def] = storageContext[def] else if (def.history ~= nil and def.history == true) then data[var] = storageContext[var]._getForStorage() else data[var] = storageContext[var] end end end local ok, err = pcall(persistence.store, dataFilePath, data) -- make sure there is no cache for this 'data' module package.loaded[dataFileModuleName] = nil if (not ok) then utils.log('There was a problem writing the storage values', utils.LOG_ERROR) utils.log(err, utils.LOG_ERROR) end end end local function getEventInfo(eventHandler, mode) local res = {} res.type = mode res.scriptName = eventHandler.name if (eventHandler.trigger ~= nil) then res.trigger = eventHandler.trigger end return res end local function deprecationWarning(moduleName, key, value, quoted) local msg if quoted then msg = 'dzVents deprecation warning: please use "on = { [\'' .. key .. '\'] = { \'' .. tostring(value) .. '\' } }". Module: ' .. moduleName else msg = 'dzVents deprecation warning: please use "on = { [\'' .. key .. '\'] = { ' .. tostring(value) .. ' } }". Module: ' .. moduleName end utils.log(msg, utils.LOG_ERROR) end function self.callEventHandler(eventHandler, subject) local useStorage = false if (eventHandler['execute'] ~= nil) then -- ================== -- Prepare storage -- ================== if (eventHandler.data ~= nil) then useStorage = true local localStorageContext = self.getStorageContext(eventHandler.data, eventHandler.dataFileName) if (localStorageContext) then self.domoticz.data = localStorageContext else self.domoticz.data = {} end end if (globalsDefinition) then local globalStorageContext = self.getStorageContext(globalsDefinition, '__data_global_data') self.domoticz.globalData = globalStorageContext else self.domoticz.globalData = {} end -- ================== -- Run script -- ================== local ok, res, info local baseType = subject and subject.baseType or '' if (baseType == domoticz.BASETYPE_DEVICE) then info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_DEVICE) ok, res = pcall(eventHandler['execute'], self.domoticz, subject, info) elseif (baseType == domoticz.BASETYPE_VARIABLE) then info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_VARIABLE) ok, res = pcall(eventHandler['execute'], self.domoticz, subject, info) elseif (baseType == domoticz.BASETYPE_SECURITY) then info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_SECURITY) local security = Security(self.domoticz, info.trigger) ok, res = pcall(eventHandler['execute'], self.domoticz, security, info) elseif (baseType == domoticz.BASETYPE_SCENE or baseType == domoticz.BASETYPE_GROUP) then if (baseType == domoticz.BASETYPE_SCENE) then info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_SCENE) else info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_GROUP) end ok, res = pcall(eventHandler['execute'], self.domoticz, subject, info) elseif (baseType == domoticz.BASETYPE_SHELLCOMMAND_RESPONSE) then info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_SHELLCOMMANDRESPONSE) info.trigger = subject.callback local response = ShellCommandResponse(self.domoticz, subject) ok, res = pcall(eventHandler['execute'], self.domoticz, response, info) elseif (baseType == domoticz.BASETYPE_HTTP_RESPONSE) then info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_HTTPRESPONSE) info.trigger = subject.callback local response = HTTPResponse(self.domoticz, subject) ok, res = pcall(eventHandler['execute'], self.domoticz, response, info) elseif (baseType == domoticz.BASETYPE_SYSTEM_EVENT) then info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_DOMOTICZ) info.trigger = subject.type local dze = SystemEvent(self.domoticz, subject) ok, res = pcall(eventHandler['execute'], self.domoticz, dze, info) elseif (baseType == domoticz.BASETYPE_CUSTOM_EVENT) then info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_CUSTOM) info.trigger = subject.type local custom = CustomEvent(self.domoticz, subject) ok, res = pcall(eventHandler['execute'], self.domoticz, custom, info) else -- timer info = getEventInfo(eventHandler, self.domoticz.EVENT_TYPE_TIMER) local timer = Timer(self.domoticz, info.trigger) ok, res = pcall(eventHandler['execute'], self.domoticz, timer, info) end if (ok) then -- ================== -- Persist storage -- ================== if (useStorage) then self.writeStorageContext(eventHandler.data, eventHandler.dataFilePath, eventHandler.dataFileName, self.domoticz.data) end if (globalsDefinition) then self.writeStorageContext(globalsDefinition, _G.dataFolderPath .. '/__data_global_data.lua', _G.dataFolderPath .. '/__data_global_data', self.domoticz.globalData) end self.domoticz.data = nil self.domoticz.globalData = nil return res else utils.log('An error occurred when calling event handler ' .. eventHandler.name, utils.LOG_ERROR) utils.log(res, utils.LOG_ERROR) -- error info end else utils.log('No "execute" function found in event handler ' .. eventHandler, utils.LOG_ERROR) end self.domoticz.data = nil self.domoticz.globalData = nil end function self.scandir(directory, type) local pos, len local i, t, popen = 0, {}, io.popen local cmd local namesLookup = {} if (directory == nil) then return {} end if (sep == '/') then cmd = 'ls -a "' .. directory .. '"' else -- assume windows for now cmd = 'dir "' .. directory .. '" /B' end t = {} local pfile = popen(cmd) for filename in pfile:lines() do pos, len = string.find(filename, '.lua', 1, true) if (pos and pos > 0 and filename:sub(1, 1) ~= '.' and len == string.len(filename)) then local name = string.sub(filename, 1, pos - 1) table.insert(t, { ['type'] = type, ['name'] = name }) namesLookup[name] = true -- for quick lookup for filename --utils.log('Found module in ' .. directory .. ' folder: ' .. t[#t].name, utils.LOG_DEBUG) end end pfile:close() return t, namesLookup end function self.processTimeRuleFunction(fn) local ok, res = pcall(fn, self.domoticz) if (not ok) then utils.log('Error executing custom timer function.', utils.LOG_ERROR) utils.log(res, utils.LOG_ERROR) if (_G.TESTMODE) then print(res) end return false end return res end function self.handleEvents(events, subject) local originalLogLevel = _G.logLevel -- a script can override the level local function restoreLogging() _G.logLevel = originalLogLevel _G.logMarker = nil end if (type(events) ~= 'table') then restoreLogging() return end for eventIdx, eventHandler in pairs(events) do if (eventHandler.logging) then if type(eventHandler.logging) ~= 'table' then if type(eventHandler.logging) == 'number' then local level = eventHandler.logging eventHandler.logging = {} eventHandler.logging.level = level end utils.log(eventHandler.type .. ' script ' .. eventHandler.name .. '.lua has a malformed logging section. Check the documentation.', utils.LOG_FORCE) end if (eventHandler.logging.level ~= nil) then _G.logLevel = eventHandler.logging.level end if (eventHandler.logging.marker ~= nil) then _G.logMarker = eventHandler.logging.marker end end local moduleLabel local moduleLabelInfo = '' local triggerInfo local scriptType = eventHandler.type == 'external' and 'external script: ' or 'internal script: ' if (eventHandler.type == 'external') then moduleLabel = eventHandler.name .. '.lua' else moduleLabel = eventHandler.name .. '' end domoticz.moduleLabel = eventHandler.name:gsub('%.lua','') -- used in dynamic logmarker local baseType = subject and subject.baseType or '' if (baseType == domoticz.BASETYPE_DEVICE) then moduleLabelInfo = ' Device: "' .. subject.name .. ' (' .. subject.hardwareName .. ')", Index: ' .. tostring(subject.id) elseif (baseType == domoticz.BASETYPE_VARIABLE) then moduleLabelInfo = ' Variable: "' .. subject.name .. '" Index: ' .. tostring(subject.id) elseif (baseType == domoticz.BASETYPE_SECURITY) then moduleLabelInfo = ' Security: "' .. subject.name .. '"' elseif (baseType == domoticz.BASETYPE_SCENE or baseType == domoticz.BASETYPE_GROUP) then moduleLabelInfo = (subject.baseType == 'scene' and ' Scene' or ' Group') .. ': "' .. subject.name .. '", Index: ' .. tostring(subject.id) elseif (baseType == domoticz.BASETYPE_HTTP_RESPONSE) then moduleLabelInfo = ' HTTPResponse: "' .. subject.callback .. '"' elseif (baseType == domoticz.BASETYPE_SHELLCOMMAND_RESPONSE) then moduleLabelInfo = ' ShellCommandResponse: "' .. subject.callback .. '"' elseif (baseType == domoticz.BASETYPE_SYSTEM_EVENT) then moduleLabelInfo = ' Domoticz event: "' .. subject.name .. '"' elseif (baseType == domoticz.BASETYPE_CUSTOM_EVENT) then moduleLabelInfo = ' Custom event: "' .. subject.name .. '"' end triggerInfo = eventHandler.trigger and ( ', trigger: "' .. eventHandler.trigger .. '"' ) or '' local clockTimeStampAtStart = os.clock() local timeStampAtStart = os.time() utils.log('------ Start ' .. scriptType .. moduleLabel ..':' .. moduleLabelInfo .. triggerInfo, utils.LOG_MODULE_EXEC_INFO) self.callEventHandler(eventHandler, subject) local clockTimeSpend = os.clock() - clockTimeStampAtStart local realTimeSpend = os.time() - timeStampAtStart if realTimeSpend > 9 or clockTimeSpend > 7 then utils.log('------ Finished ' .. moduleLabel .. ' after >' .. realTimeSpend .. ' seconds. (using '.. tostring(clockTimeSpend):sub(1,5) .. ' seconds CPU time !)' , utils.LOG_ERROR) elseif realTimeSpend > 6 or clockTimeSpend > 5 then utils.log('------ Finished ' .. moduleLabel .. ' after >' .. realTimeSpend .. ' seconds. (using '.. tostring(clockTimeSpend):sub(1,5) .. ' seconds CPU time !)' , utils.LOG_FORCE) else utils.log('------ Finished ' .. moduleLabel , utils.LOG_MODULE_EXEC_INFO) end if (tonumber(_gv['dzVents_log_level']) == utils.LOG_DEBUG or TESTMODE ) then local moduleSummary = _gv.script_path .. 'module.log' utils.log('Debug: Writing module summary to ' .. moduleSummary ,utils.LOG_FORCE) local f = io.open(moduleSummary, 'a' ) f:write( os.date('%x %X - ',timeStampAtStart) .. os.date('%x %X ') .. '(' .. string.format('%02d', realTimeSpend) .. ' - ' .. string.format('%.4f',clockTimeSpend) .. ') ' .. string.format('%35s',moduleLabel) .. ' <<' .. moduleLabelInfo .. ( eventHandler.trigger and ( ' timer: "' .. eventHandler.trigger .. '"' ) or '') ,'\n') f:close() end restoreLogging() end end function self.processTimeRules(timeRules, testTime) -- accepts a table of timeDefs, if one of them matches with the -- current time, then it returns true -- otherwise it returns false local now if (testTime == nil) then now = self.domoticz.time else now = testTime end for i, _rule in pairs(timeRules) do if (type(_rule) == 'function') then return self.processTimeRuleFunction(_rule), 'function' end local rule = string.lower(tostring(_rule)) if (now.matchesRule(rule)) then return true, _rule end end return false end function self.checkSecurity(securityDefs, security) for i, def in pairs(securityDefs) do if (def == security) then return true, def end end return false end local function addBindingEvent(bindings, event, module) if (bindings[event] == nil) then bindings[event] = {} end table.insert(bindings[event], module) end local function loadEventScripts() local scripts = {} local errModules = {} local modules = {} local scripts = {} local requireTimeout, moduleName, event, err, ok, diskScripts, externalNames, internalScripts ok, diskScripts, externalNames = pcall(self.scandir, _G.scriptsFolderPath, 'external') if (not ok) then utils.log(diskScripts, utils.LOG_ERROR) else modules = diskScripts end ok = true ok, internalScripts = pcall(self.scandir, _G.generatedScriptsFolderPath, 'internal') if (not ok) then utils.log(internalScripts, utils.LOG_ERROR) else for i, internal in pairs(internalScripts) do if (externalNames[internal.name]) then -- oops already there, skipping utils.log('There is already an external script with the name "' .. internal.name .. '.lua". Please rename in the internal script.', utils.LOG_ERROR) else table.insert(modules, internal) end end end -- extract global_data module and make sure it is the first in the list -- because then peeps can create some globals available everywhere before the other modules load local globalIx for i, moduleInfo in pairs(modules) do if (moduleInfo.name == GLOBAL_DATA_MODULE) then globalIx = i break end end if (globalIx ~= nil and globalIx > 1) then local globalModule = modules[globalIx] table.remove(modules, globalIx) table.insert(modules, 1, globalModule) end function self.wait(time) local isWindows = string.sub(package.config, 1, 1) ~= '/' local delayCommand = ( isWindows and 'timeout /T 1' ) or 'sleep 1' local duration = os.time() + ( time or 1 ) while os.time() < duration do os.execute(delayCommand) end end function self.tryRequire(moduleName, requireTimeout, location) local ok = true ok, module = pcall(require, moduleName) if ( ok and module ) or requireTimeout < 1 or not(tostring(module):match('^module')) then return ok, module, requireTimeout elseif not(ok) then while not(ok) and requireTimeout > 0 do utils.log('Loading of ' .. location .. ' script "'.. moduleName .. '.lua" failed. Retrying in a second.', utils.LOG_FORCE) if not(_G.TESTMODE) then self.wait(1) end ok, module = pcall(require, moduleName) if not(ok) then requireTimeout = requireTimeout - 1 end end end return ok, module, requireTimeout end for i, moduleInfo in ipairs(modules) do local module, skip local moduleName = moduleInfo.name local logScript = (moduleInfo.type == 'external' and 'Script ' or 'Internal script ') _G.domoticz = { LOG_INFO = utils.LOG_INFO, LOG_MODULE_EXEC_INFO = utils.LOG_MODULE_EXEC_INFO, LOG_DEBUG = utils.LOG_DEBUG, LOG_ERROR = utils.LOG_ERROR, LOG_FORCE = utils.LOG_FORCE, SECURITY_DISARMED = self.domoticz.SECURITY_DISARMED, SECURITY_ARMEDAWAY = self.domoticz.SECURITY_ARMEDAWAY, SECURITY_ARMEDHOME = self.domoticz.SECURITY_ARMEDHOME, } local ok = true ok, module, requireTimeout = self.tryRequire(moduleName, ( requireTimeout or 3 ), moduleInfo.type ) _G.domoticz = nil if (ok) then if (moduleName == GLOBAL_DATA_MODULE) then if (module.data ~= nil) then globalsDefinition = module.data if (_G.TESTMODE) then self.globalsDefinition = globalsDefinition end end if (module.helpers ~= nil) then self.domoticz.helpers = module.helpers end else if (type(module) == 'table') then skip = false if (module.active ~= nil) then local active = false if (type(module.active) == 'function') then active = module.active(self.domoticz) else active = module.active end if (not active) then skip = true end end if (not skip) then if (module.on ~= nil and module['execute'] ~= nil) then module.name = moduleName module.type = moduleInfo.type module.dataFileName = '__data_' .. moduleName module.dataFilePath = _G.dataFolderPath .. '/__data_' .. moduleName .. '.lua' table.insert(scripts, module) else utils.log(logScript .. moduleName .. '.lua has no "on" and/or "execute" section, not a dzVents module. Skipping', utils.LOG_DEBUG) --table.insert(errModules, moduleName) end end else utils.log(logScript .. moduleName .. '.lua is not a dzVents module. Skipping', utils.LOG_DEBUG) --table.insert(errModules, moduleName) end end else table.insert(errModules, moduleName) utils.log(module, utils.LOG_ERROR) end end return scripts, errModules end function self.getEventBindings(mode, testTime) local bindings = {} local ok, i, event, j, device, err local modules = {} if not self.scripts then self.scripts, self.errModules = loadEventScripts() end if (mode == nil) then mode = 'device' end for i, module in pairs(self.scripts) do local logScript = (module.type == 'external' and 'Script ' or 'Internal script ') for j, event in pairs(module.on) do if type(j) ~= 'string' or type(event) ~= 'table' or validEventTypes:find(j) == nil then if not self.scripts[i].invalidOnSection then self.scripts[i].invalidOnSection = true if type(j) == "string" and validEventTypes:find(j) == nil then if inValidEventTypes:find(j) then utils.log('You entered "' .. tostring(j) .. '" in the on = section. Probably you misplaced your curly brackets', utils.LOG_FORCE) else utils.log('Valid eventTypes are: ' .. validEventTypes, utils.LOG_DEBUG ) utils.log('You entered "' .. tostring(j) .. '" in the on = section. Maybe you meant "' .. utils.fuzzyLookup(j, utils.stringSplit(validEventTypes,',')) ..'"?', utils.LOG_FORCE) end else utils.log('You entered "' .. utils.toStr(event) .. '" as trigger but the eventType is not (properly) set') end utils.log(logScript .. module.name .. '.lua has an invalid on = section; please check the wiki. Skipping it until fixed.', utils.LOG_ERROR) end else if (mode == 'timer' and j == 'timer') then -- { ['timer'] = { 'every minute ', 'every hour' } } if type(event) ~= 'table' then utils.log(logScript .. module.name .. '.lua has a malformed timer = section. Check the documentation.', utils.LOG_FORCE) event = { event } end local triggered, def = self.processTimeRules(event) if (triggered) then -- this one can be executed module.trigger = def event.type = 'timer' table.insert(bindings, module) end elseif (mode == 'device' and j == 'devices') then -- { ['devices'] = { 'devA', ['devB'] = { ..timedefs }, .. } for devIdx, devName in pairs(event) do -- detect if devName is of the form ['devB'] = { 'every hour' } if (type(devName) == 'table') then local triggered, def = self.processTimeRules(devName, testTime) if (triggered) then addBindingEvent(bindings, devIdx, module) end else -- a single device name (or id) addBindingEvent(bindings, devName, module) end end elseif (mode == 'scenegroups' and (j == 'scenes' or j == 'groups')) then -- { ['scenes'] = { 'scA', ['scB'] = { ..timedefs }, .. } for scgrpIdx, scgrpName in pairs(event) do -- detect if scgrpName is of the form ['devB'] = { 'every hour' } if (type(scgrpName) == 'table') then local triggered, def = self.processTimeRules(scgrpName, testTime) if (triggered) then addBindingEvent(bindings, scgrpIdx, module) end else -- a single scene or group name (or id) addBindingEvent(bindings, scgrpName, module) end end elseif (mode == 'variable' and j == 'variables') then -- { ['variables'] = { 'varA', 'varB' } for varIdx, varName in pairs(event) do addBindingEvent(bindings, varName, module) end elseif (mode == 'security' and j == 'security') then local triggered, def = self.checkSecurity(event, self.domoticz.security) if (triggered) then table.insert(bindings, module) module.trigger = def end elseif (mode == 'httpResponse' and j == 'httpResponses') then -- { ['httpResponses'] = { 'callbackA', 'callbackB' } for i, callbackName in pairs(event) do addBindingEvent(bindings, callbackName, module) end elseif (mode == 'shellcommandResponse' and j == 'shellCommandResponses') then -- { ['shellCommandResponses'] = { 'callbackA', 'callbackB' } for i, callbackName in pairs(event) do addBindingEvent(bindings, callbackName, module) end elseif (mode == 'system' and j == 'system') then -- { ['system'] = { 'start', 'end' } for evtIdx, systemEvent in pairs(event) do -- detect if system is of the form ['start'] = { 'every hour' } if (type(systemEvent) == 'table') then local triggered, def = self.processTimeRules(systemEvent, testTime) if (triggered) then addBindingEvent(bindings, evtIdx, module) end else -- a single system event addBindingEvent(bindings, systemEvent, module) end end elseif (mode == 'customEvents' and j == 'customEvents') then -- { customEvents = { 'myEvent', ['anotherEvent'] = { ... timeDefs ... } } for evtIdx, customEventName in pairs(event) do -- detect if customEvent is of the form ['myEvent'] = { 'every hour' } if (type(customEventName) == 'table') then local triggered, def = self.processTimeRules(customEventName, testTime) if (triggered) then addBindingEvent(bindings, evtIdx, module) end else -- a single custom event addBindingEvent(bindings, customEventName, module) end end end end end end return bindings, self.errModules end function self.dumpCommandArray(commandArray, fromIndex, force) local printed = false local level = utils.LOG_DEBUG if (fromIndex == nil) then fromIndex = 1 end if (force == true and force ~= nil or _gv['testmode'] == true) then level = utils.LOG_INFO end for k, v in pairs(commandArray) do if ((fromIndex ~= nil and k >= fromIndex) or fromIndex == nil) then if (not printed) then utils.log('Commands sent to Domoticz: ', level) end if (type(v) == 'table') then for kk, vv in pairs(v) do utils.log('- ' .. kk .. ' = ' .. utils.toStr(vv), level) end else utils.log(k .. ' = ' .. v, level) end printed = true end end if (printed) then utils.log('=====================================================', level) end end function self.findScriptForTarget(target, allEventScripts) -- event could be like: myPIRLivingRoom -- or myPir(.*) --[[ allEventScripts is a dictionary where each key is the name or id of a device and the value is a table with all the modules for this device { ['myDevice'] = { modA, modB, modC }, ['anotherDevice'] = { modD }, 12 = { modE, modF }, ['myDev*'] = { modG, modH } } ]]-- local modules -- only search for named and wildcard triggers, -- id is done later for scriptTrigger, scripts in pairs(allEventScripts) do local function strFind(str, key) return string.find(str, key) end local ok, res = pcall(strFind, scriptTrigger, '*'); if not ok then utils.log('Script name: ' .. scripts[1].name .. '.lua, has a malformed on = section. The trigger = ' .. utils.toStr(scriptTrigger) , utils.LOG_ERROR) allEventScripts[scriptTrigger] = '' elseif res then -- a wild-card was used scriptTrigger = ('^' .. scriptTrigger:gsub("[%^$]","."):gsub("*", ".*") .. '$') local function sMatch(text, match) -- specialized sanitized match function to allow combination of Lua magic chars in wildcards local sanitizedMatch = match:gsub("([%%%(%)%[%]%+%-%?])", "%%%1") -- escaping all 'magic' chars except *, ., ^ and $ return text:match(sanitizedMatch) end if sMatch(target, scriptTrigger) then if modules == nil then modules = {} end for i, mod in pairs(scripts) do table.insert(modules, mod) end end else if (scriptTrigger == target) then -- there is trigger for this target if modules == nil then modules = {} end for i, mod in pairs(scripts) do table.insert(modules, mod) end end end end return modules end function self.dispatchDeviceEventsToScripts(domoticz) if (domoticz == nil) then -- you can pass a domoticz object for testing purposes domoticz = self.domoticz end local allEventScripts = self.getEventBindings() domoticz.changedDevices().forEach( function(device) local scriptsToExecute = self.findScriptForTarget(device.name, allEventScripts) local idScripts = allEventScripts[device.id] local caSize = _.size(self.domoticz.commandArray) if (idScripts ~= nil) then -- merge id scripts with name scripts if (scriptsToExecute == nil) then scriptsToExecute = {} end for i, mod in pairs(idScripts) do table.insert(scriptsToExecute, mod) end end if (scriptsToExecute ~= nil) then utils.log('Handling events for: "' .. device.name .. '", value: "' .. tostring(device.state) .. '"', utils.LOG_MODULE_EXEC_INFO) self.handleEvents(scriptsToExecute, device) self.dumpCommandArray(self.domoticz.commandArray, caSize + 1) end end) return self.domoticz.commandArray end function self.dispatchSceneGroupEventsToScripts(domoticz) if (domoticz == nil) then -- you can pass a domoticz object for testing purposes domoticz = self.domoticz end local allEventScripts = self.getEventBindings('scenegroups') local processItem = function(item, loglabel) utils.log(loglabel .. '-event for: ' .. item.name .. ' value: ' .. tostring(item.state), utils.LOG_DEBUG) local scriptsToExecute = self.findScriptForTarget(item.name, allEventScripts) local idScripts = allEventScripts[item.id] local caSize = _.size(self.domoticz.commandArray) if (idScripts ~= nil) then -- merge id scripts with name scripts if (scriptsToExecute == nil) then scriptsToExecute = {} end for i, mod in pairs(idScripts) do table.insert(scriptsToExecute, mod) end end if (scriptsToExecute ~= nil) then utils.log('Handling events for: "' .. item.name .. '", value: "' .. tostring(item.state) .. '"', utils.LOG_MODULE_EXEC_INFO) self.handleEvents(scriptsToExecute, item) self.dumpCommandArray(self.domoticz.commandArray, caSize + 1) end end domoticz.changedScenes().forEach(function(scene) processItem(scene, 'Scene') end) domoticz.changedGroups().forEach(function(group) processItem(group, 'Group') end) return self.domoticz.commandArray end function self.dispatchTimerEventsToScripts() local scriptsToExecute = self.getEventBindings('timer') self.handleEvents(scriptsToExecute) self.dumpCommandArray(self.domoticz.commandArray) return self.domoticz.commandArray end function self.dispatchSecurityEventsToScripts() local updates =_G.securityupdates if (updates ~= nil) then for i, securityState in pairs(updates) do local security = { baseType = domoticz.BASETYPE_SECURITY, name = securityState } local caSize = _.size(self.domoticz.commandArray) self.domoticz.security = securityState local scriptsToExecute = self.getEventBindings('security', nil) self.handleEvents(scriptsToExecute, security) self.dumpCommandArray(self.domoticz.commandArray, caSize + 1) end end return self.domoticz.commandArray end function self.dispatchVariableEventsToScripts(domoticz) if (domoticz == nil) then -- you can pass a domoticz object for testing purposes domoticz = self.domoticz end local allEventScripts = self.getEventBindings('variable') domoticz.changedVariables().forEach(function(variable) local caSize = _.size(self.domoticz.commandArray) -- first search by name local scriptsToExecute = self.findScriptForTarget(variable.name, allEventScripts) if (scriptsToExecute == nil) then -- search by id scriptsToExecute = allEventScripts[variable.id] end if (scriptsToExecute ~= nil) then utils.log('Handling variable-events for: "' .. variable.name .. '", value: "' .. tostring(variable.value) .. '"', utils.LOG_MODULE_EXEC_INFO) self.handleEvents(scriptsToExecute, variable) self.dumpCommandArray(self.domoticz.commandArray, caSize + 1) end end) return self.domoticz.commandArray end function self.dispatchShellCommandResponseEventsToScripts(domoticz) if (domoticz == nil) then -- you can pass a domoticz object for testing purposes domoticz = self.domoticz end local shellcommandResponseScripts = self.getEventBindings('shellcommandResponse') local responses =_G.shellcommandresponse if (responses ~= nil) then for i, response in pairs(responses) do response.baseType = domoticz.BASETYPE_SHELLCOMMAND_RESPONSE local callback = response.callback local caSize = _.size(self.domoticz.commandArray) local scriptsToExecute = self.findScriptForTarget(callback, shellcommandResponseScripts) if (scriptsToExecute ~= nil) then utils.log('Handling shellcommandResponse-events for: "' .. callback .. '"', utils.LOG_MODULE_EXEC_INFO) self.handleEvents(scriptsToExecute, response) self.dumpCommandArray(self.domoticz.commandArray, caSize + 1) end end end return self.domoticz.commandArray end function self.dispatchHTTPResponseEventsToScripts(domoticz) if (domoticz == nil) then -- you can pass a domoticz object for testing purposes domoticz = self.domoticz end local httpResponseScripts = self.getEventBindings('httpResponse') local responses =_G.httpresponse if (responses ~= nil) then for i, response in pairs(responses) do response.baseType = domoticz.BASETYPE_HTTP_RESPONSE local callback = response.callback local caSize = _.size(self.domoticz.commandArray) local scriptsToExecute = self.findScriptForTarget(callback, httpResponseScripts) if (scriptsToExecute ~= nil) then utils.log('Handling httpResponse-events for: "' .. callback .. '"', utils.LOG_MODULE_EXEC_INFO) self.handleEvents(scriptsToExecute, response) self.dumpCommandArray(self.domoticz.commandArray, caSize + 1) end end end return self.domoticz.commandArray end function self.dispatchSystemEventsToScripts(domoticz) if (domoticz == nil) then -- you can pass a domoticz object for testing purposes domoticz = self.domoticz end if (_G.notification == nil) then return end local systemEventScripts = self.getEventBindings('system', nil) local systemEvents =_G.notification.domoticz if (systemEvents ~= nil) then for i, event in pairs(systemEvents) do event.name = SystemEvent(self.domoticz, event)["type"] if event.name == nil then utils.log('System event "' .. event.type .. '" is not supported yet.', utils.LOG_ERROR) else event.baseType = domoticz.BASETYPE_SYSTEM_EVENT local caSize = _.size(self.domoticz.commandArray) local scriptsToExecute = self.findScriptForTarget(event.name, systemEventScripts) if (scriptsToExecute ~= nil) then utils.log('Handling system event for: "' .. event.name .. '"', utils.LOG_MODULE_EXEC_INFO) self.handleEvents(scriptsToExecute, event) self.dumpCommandArray(self.domoticz.commandArray, caSize + 1) end end end end return self.domoticz.commandArray end function self.dispatchCustomEventsToScripts(domoticz) if (domoticz == nil) then -- you can pass a domoticz object for testing purposes domoticz = self.domoticz end if (_G.notification == nil or _G.notification.customevent == nil ) then return end for _, row in ipairs(_G.notification.customevent) do if row.data.name:match('^___%a*__$') then table.insert(self.domoticz.commandArray, { [ row.data.name:sub(4,-3) ] = row.data.data }) end end local customEventScripts = self.getEventBindings('customEvents', nil) local customEvents = _G.notification.customevent if (customEvents ~= nil) then for i, customEvent in pairs(customEvents) do customEvent.name = customEvent.data.name customEvent.baseType = domoticz.BASETYPE_CUSTOM_EVENT local caSize = _.size(self.domoticz.commandArray) local scriptsToExecute = self.findScriptForTarget(customEvent.name, customEventScripts) if (scriptsToExecute ~= nil) then utils.log('Handling Domoticz custom event for: "' .. customEvent.name .. '"', utils.LOG_MODULE_EXEC_INFO) self.handleEvents(scriptsToExecute, customEvent) self.dumpCommandArray(self.domoticz.commandArray, caSize + 1) end end end return self.domoticz.commandArray end function self.getEventSummary(domoticz) if (domoticz == nil) then -- you can pass a domoticz object for testing purposes domoticz = self.domoticz end local items = {} local length = 0 domoticz.changedDevices().forEach( function(device) table.insert(items, '- Device: ' .. device.name) length = length + 1 end) domoticz.changedVariables().forEach(function(variable) table.insert(items, '- Variable: ' .. variable.name) length = length + 1 end) local securityUpdates =_G.securityupdates if (securityUpdates ~= nil) then for i, securityState in pairs(securityUpdates) do table.insert(items, '- Security: ' .. securityState) length = length + 1 end end domoticz.changedScenes().forEach(function(scene) table.insert(items, '- Scene: ' .. scene.name) length = length + 1 end) domoticz.changedGroups().forEach(function(group) table.insert(items, '- Group: ' .. group.name) length = length + 1 end) local responses =_G.httpresponse if (responses ~= nil) then for i, response in pairs(responses) do table.insert(items, '- HTTPResponse: ' .. response.callback) length = length + 1 end end local domoticzEvents = _G.notification and _G.notification.domoticz or nil if (domoticzEvents ~= nil) then for i, domoticzEvent in pairs(domoticzEvents) do table.insert(items, '- Domoticz: ' .. domoticzEvent.type) -- .. ' - ' .. domoticzEvent.status) length = length + 1 end end local customEvents = _G.notification and _G.notification.customEvents or nil if (customEvents ~= nil) then for i, customEvent in pairs(customEvents) do table.insert(items, '- Custom: ' .. customEvent.type) -- .. ' - ' .. customEvent.status) length = length + 1 end end return items, length; end return self end return EventHelpers