-- MiOS "Smart Switch" Plugin
--
-- Copyright (C) 2014 Hugh Eaves
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see .
-- 2021.02.26 @akbooer (for @DesT)
-- optionally use device NAMES to override IDS
-- 2021.02.27 add SensorNames functionality
-- 2021.07.26 @akbooer, add trigger device name(s) to device panel (for @DesT)
-- 2021.07.26b small fix to above
g_pluginName = "SmartSwitch"
-- IMPORT GLOBALS
local luup = luup
local string = string
local require = require
local math = math
local log = require("L_" .. g_pluginName .. "_" .. "log")
local util = require("L_" .. g_pluginName .. "_" .. "util")
local json = require("dkjson")
-- CONSTANTS
-- Plug-in version
--local PLUGIN_VERSION = "2.0.2" -- 2021.02.27 @akbooer
local PLUGIN_VERSION = "2.0.3" -- 2021.07.26 @akbooer
local LOG_PREFIX = "SmartSwitch"
local DATE_FORMAT = "%m/%d/%y %H:%M:%S"
-- value assigned to timeout when there is no timeout
local FAR_FUTURE_TIME = 2147483647
-- current "mode" of smart switch
local MODE = {
OFF = "Off",
AUTO = "Auto",
MANUAL = "Manual"
}
local PRE_TIMEOUT_POLL_DELAY = 5
-- TASK status stolen from mios_vista-alarm-panel
local TASK = {
ERROR = 2,
ERROR_PERM = -2,
SUCCESS = 4,
BUSY = 1
}
local SID = {
SWITCH = "urn:upnp-org:serviceId:SwitchPower1",
DIMMER = "urn:upnp-org:serviceId:Dimming1",
SMART_SWITCH = "urn:hugheaves-com:serviceId:SmartSwitch1",
SMART_SWITCH_CONTROLLER = "urn:hugheaves-com:serviceId:SmartSwitchController1",
SECURITY_SENSOR = "urn:micasaverde-com:serviceId:SecuritySensor1",
ZWAVE_DEVICE = "urn:micasaverde-com:serviceId:ZWaveDevice1",
HA_DEVICE = "urn:micasaverde-com:serviceId:HaDevice1"
}
local DID_SMART_SWITCH_CONTROLLER = "urn:schemas-hugheaves-com:device:SmartSwitchController:1"
local DEFAULT_LOG_CONFIG = {
["version"] = 1,
["files"] = {
["./*L_SmartSwith_log.lua$"] = {
["level"] = log.LOG_LEVEL_DEBUG,
["functions"] = {
}
},
["./*L_SmartSwitch_util.lua$"] = {
["level"] = log.LOG_LEVEL_DEBUG,
["functions"] = {
}
},
}
}
local logConfig = DEFAULT_LOG_CONFIG
-- GLOBALS
-- Maps switch devices to smart switch ids, and a list of sensors for that switch
g_switches = {
-- smartSwitchId
-- sensors
}
-- Maps sensor device ids to switches that use that sensor
g_sensors = {
-- switches
}
-- Maps a smart switch id to a switch id
g_smartSwitches = {
-- switchId
}
-- Holds a stack of currently scheduled checkSwitches calls
g_scheduledCalls = {}
g_deviceId = nil
g_taskHandle = -1
----------------------------------------------------
-- FUNCTIONS
----------------------------------------------------
local getFormattedLevel = function (level)
if (level == 0) then
return "Off"
elseif (level == 100) then
return "On"
end
return tostring(level) .. "%"
end
local getFormattedTimeout = function (timeout)
local hours = math.floor(timeout / 3600)
local hoursRemainder = timeout % 3600
if (hours > 0) then
return tostring(hours) .. "h"
end
local minutes = math.floor(hoursRemainder / 60)
local minutesRemainder = hoursRemainder % 60
if (minutes > 0) then
return tostring(minutes) .. "m"
end
local secondes = math.floor(minutesRemainder / 60)
return tostring(secondes) .. "s"
end
-- Shows Smartswitch Controller status on UI
local function showStatusOnUI (smartSwitchId)
local statusText = {'
'}
local function p(...) for _, x in ipairs {...} do statusText[#statusText+1] = x end end
-- Levels
local onLevel = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "OnLevel", smartSwitchId, util.T_NUMBER)
local offLevel = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "OffLevel", smartSwitchId, util.T_NUMBER)
p ("
Levels: On/", getFormattedLevel(onLevel), " Off/", getFormattedLevel(offLevel), "
")
-- Timeouts
local autoTimeout = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "AutoTimeout", smartSwitchId, util.T_NUMBER)
local manualTimeout = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "ManualTimeout", smartSwitchId, util.T_NUMBER)
p ("
Timeouts: Auto/", getFormattedTimeout(autoTimeout), " Manual/", getFormattedTimeout(manualTimeout), "
")
-- Current status
p "
Mode: "
local currentMode = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Mode", smartSwitchId, util.T_STRING)
local currentLevel = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Level", smartSwitchId, util.T_NUMBER)
local currentTimeout = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Timeout", smartSwitchId, util.T_NUMBER)
if (currentMode == MODE.MANUAL) then
p ("", currentMode, "")
else
p ("", currentMode, "")
end
if (currentMode ~= MODE.OFF) then
p (" at ", tostring(currentLevel), "%")
if (currentTimeout ~= FAR_FUTURE_TIME) then
p (" until ", os.date('%H:%M:%S', currentTimeout), "")
end
end
do -- 2021.07.26, @akbooer, trigger device name(s)
local sensorNames = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "SensorNames", smartSwitchId, util.T_TABLE)
local ellipsis = (#sensorNames > 1) and ", …" or ''
p (" Name: ", sensorNames[1] or '?', ellipsis, "")
end
p "
"
p "
"
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "StatusText", table.concat (statusText), smartSwitchId)
end
-- Set light level on target switch
local function setSwitchLevel(switchId, level)
log.infoValues ("Setting Switch Level", "switchId", switchId, "level", level)
local lul_settings = {}
local lul_resultcode, lul_resultstring, lul_job, lul_returnarguments
local smartSwitchId = g_switches[tonumber(switchId)].smartSwitchId
-- If the target device is a dimmer
if (luup.device_supports_service(SID.DIMMER, tonumber(switchId))) then
lul_settings.newLoadlevelTarget = level
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Level", level, smartSwitchId)
lul_resultcode, lul_resultstring, lul_job, lul_returnarguments = luup.call_action(SID.DIMMER,
"SetLoadLevelTarget", lul_settings, tonumber(switchId))
-- else, if the target device is a binary switch
elseif (luup.device_supports_service(SID.SWITCH, tonumber(switchId))) then
if (level == 0) then
lul_settings.newTargetValue = 0
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Level", "0", smartSwitchId)
else
lul_settings.newTargetValue = 1
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Level", "100", smartSwitchId)
end
local lul_resultcode, lul_resultstring, lul_job, lul_returnarguments = luup.call_action(SID.SWITCH,
"SetTarget", lul_settings, tonumber(switchId))
end
log.debug("Returning")
end
local function turnOffSwitch(smartSwitchId)
-- get the device id for this smart switch
local switchId = g_smartSwitches[smartSwitchId].switchId
log.infoValues ("Turning off switch", "switchId", switchId, "currentMode",
util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Mode", smartSwitchId, util.T_STRING))
setSwitchLevel(switchId, util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "OffLevel", smartSwitchId, util.T_NUMBER))
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Mode", MODE.OFF, smartSwitchId)
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Timeout", FAR_FUTURE_TIME, smartSwitchId)
showStatusOnUI(smartSwitchId)
end
-- Sets the light level of the switch to match the current OnLevel/OffLevel
local function updateSwitchLevel(smartSwitchId)
log.info ("Updating Switch Level: smartSwitchId = ", smartSwitchId)
local currentMode = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Mode", smartSwitchId, util.T_STRING)
local switchId = g_smartSwitches[smartSwitchId].switchId
if (currentMode == MODE.AUTO) then
setSwitchLevel(switchId, util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "OnLevel", smartSwitchId, util.T_NUMBER))
elseif (currentMode == MODE.OFF) then
setSwitchLevel(switchId, util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "OffLevel", smartSwitchId, util.T_NUMBER))
end
showStatusOnUI(smartSwitchId)
end
----------------------------------------------
------ TIMEOUT SCHEDULING / HANDLING ---------
----------------------------------------------
--
local function checkSwitch(currentTime, smartSwitchId)
local switchId = g_smartSwitches[smartSwitchId].switchId
local timeout = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Timeout", smartSwitchId, util.T_NUMBER)
local nextWakeup = timeout
log.debugValues ("Current switch timeout", "smartSwitchId",smartSwitchId, "timeout", os.date(DATE_FORMAT, timeout))
-- if the timeout has expired for this switch (i.e. timeout is before currentTime), then turn it off
if (timeout <= currentTime) then
log.infoValues("Timeout has expired, turning off switch", "smartSwitchId", smartSwitchId, "switchId", switchId)
turnOffSwitch(smartSwitchId)
elseif (timeout ~= FAR_FUTURE_TIME) then
-- else, if the switch hasn't yet timed out
-- handle scheduling a "pre-timeout" poll for Z-wave devices
if (luup.device_supports_service(SID.ZWAVE_DEVICE, tonumber(switchId))) then
-- calculate when we should be polling the switch
local pollTime = timeout - PRE_TIMEOUT_POLL_DELAY
-- if pollTime has arrived (or passed), then check if we have already polled the switch recently
if (pollTime <= currentTime) then
local lastPollTime = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "LastPollTime", smartSwitchId, util.T_NUMBER)
if (currentTime - lastPollTime > PRE_TIMEOUT_POLL_DELAY) then
log.debugValues("Polling switch before timeout", "timeout", timeout, "pollTime", pollTime, "lastPollTime", lastPollTime , "currentTime", currentTime)
luup.call_action(SID.HA_DEVICE, "Poll", {}, switchId)
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "LastPollTime", currentTime, smartSwitchId)
else
log.debugValues("Switch poll not needed", "timeout", timeout, "pollTime", pollTime, "lastPollTime", lastPollTime , "currentTime", currentTime)
end
else
-- as pollTime has not arrived yet, schedule next wakeup at pollTime
nextWakeup = pollTime
log.debugValues("pollTime has not arrived, scheduling wakeup at pollTime", "timeout", timeout, "pollTime", pollTime , "currentTime", currentTime)
end
else
log.debugValues("target switch is not a ZWave device, scheduling wakeup at normal timeout", "timeout", timeout, "currentTime", currentTime)
end
end
return nextWakeup
end
-- Find the next (earliest) scheduled call time in the g_scheduledCalls table
local function getNextWakeup()
log.debugValues("", "#g_scheduledCalls", #g_scheduledCalls)
local nextWakeup = FAR_FUTURE_TIME
for i = #g_scheduledCalls, 1, -1 do
log.debug ("g_scheduledCalls[", i, "] = ", os.date(DATE_FORMAT, g_scheduledCalls[i]))
if (g_scheduledCalls[i] < nextWakeup) then
nextWakeup = g_scheduledCalls[i]
end
end
log.debug("Next wakeup is ", os.date(DATE_FORMAT, nextWakeup))
return nextWakeup
end
-- Remove the next (earliest) scheduled call time in the g_scheduledCalls table
local function removeNextWakeup()
log.debugValues("", "#g_scheduledCalls", #g_scheduledCalls)
local nextWakeup = FAR_FUTURE_TIME
local foundIndex = 0;
for i = #g_scheduledCalls, 1, -1 do
log.debug ("g_scheduledCalls[", i, "] = ", os.date(DATE_FORMAT, g_scheduledCalls[i]))
if (g_scheduledCalls[i] < nextWakeup) then
nextWakeup = g_scheduledCalls[i]
foundIndex = i
end
end
if (foundIndex > 0) then
log.debug("removing g_scheduledCall[",foundIndex,"]")
table.remove(g_scheduledCalls, foundIndex)
end
end
-- Add a new scheduled call
local function addWakeup(currentTime, wakeupTime, functionName)
local timeRemaining = wakeupTime - currentTime
table.insert(g_scheduledCalls, wakeupTime)
luup.call_delay(functionName, timeRemaining, g_deviceId, true)
log.infoValues ("Scheduled new wakeup", "functionName", functionName, "wakeupTime", os.date(DATE_FORMAT, wakeupTime), "timeRemaining", timeRemaining)
end
--[[
Schedule a call back at "wakeupTime".
This function doesn't actually schedule a new callback for every
invocation. A new callback is not scheduled if there is an existing callback scheduled
before wakeupTime.
]]
local function scheduleNextWakeup(wakeupTime)
local currentTime = os.time()
log.debugValues ("Entering scheduleNextWakeup", "wakeupTime", os.date(DATE_FORMAT,wakeupTime), "currentTime", os.date(DATE_FORMAT,currentTime))
if (wakeupTime == FAR_FUTURE_TIME) then
log.debug("scheduleNextWakeup called with far future time. Not scheduling a call.")
return
end
-- callbacks with a checkTime in the past are scheduled at the current time instead
if (wakeupTime < currentTime) then
log.infoValues ("scheduleNextWakeup called with time in the past", "wakeupTime", os.date(DATE_FORMAT,wakeupTime), "currentTime", os.date(DATE_FORMAT,currentTime))
wakeupTime = currentTime
end
local nextWakeup = getNextWakeup()
-- only add a new callback if checkTime occurs before nextWakeup
if (wakeupTime < nextWakeup) then
log.debugValues ("Adding new wakeup call ","wakeupTime", os.date(DATE_FORMAT,wakeupTime))
addWakeup(currentTime, wakeupTime, "checkSwitches")
else
log.debug ("New call_delay of checkSwitches not needed, existing call scheduled for ", os.date(DATE_FORMAT,nextWakeup))
end
end
-- This function is called whenever an event occurs that may require
-- calculating a new timeout for a switch. This includes sensor resets,
-- manual activations, or adjustment of the timeout settings for
-- a particular switch.
local function updateSwitchTimeout(switchId)
log.debugValues ("updating timeout for switch", "switchId", switchId)
local tripped = false
local smartSwitchId = g_switches[switchId].smartSwitchId
-- Check to see if any of the sensors that control this switch are in "tripped" state.
-- If so, we don't need to do anything with the timeout value.
for sensorId, status in pairs(g_switches[switchId].sensors) do
if (util.getLuupVariable(SID.SECURITY_SENSOR, "Tripped", tonumber(sensorId), util.T_BOOLEAN)) then
log.debugValues("Sensor is still tripped", "sensorId", sensorId)
tripped = true
end
end
if (tripped) then
-- no need to schedule a timeout for a switch with a sensor that hasn't reset yet (the
-- timeout will be scheduled when the sensor actually resets).
log.debugValues ("One or more sensors for switch are still tripped. Not updating switch timeout.", "switchId", switchId)
else
local currentTime = os.time()
local newTimeout = FAR_FUTURE_TIME
local currentMode = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Mode", smartSwitchId, util.T_STRING)
if (currentMode == MODE.AUTO) then
newTimeout = currentTime + util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "AutoTimeout", smartSwitchId, util.T_NUMBER)
elseif (currentMode == MODE.MANUAL) then
newTimeout = currentTime + util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "ManualTimeout", smartSwitchId, util.T_NUMBER)
else
log.infoValues ("Sensor state reset, but switch was already turned off.", "switchId",switchId)
end
if (newTimeout ~= FAR_FUTURE_TIME) then
log.infoValues ("Setting new timeout for switch", "switchId",switchId, "newTimeout", os.date(DATE_FORMAT, newTimeout), "currentMode", currentMode)
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Timeout", newTimeout, smartSwitchId)
scheduleNextWakeup (newTimeout)
end
end
showStatusOnUI(smartSwitchId)
end
-- This is the luup.call_delay callback function that is scheduled
-- by the scheduleNextWakeup() function. It checks to see if any switches
-- have "timed out" and need to be turned back off. If there are switches
-- have not timed out, this function will schedule a new timeout
-- callback for the next occurring timeout.
function checkSwitches (data)
log.info("Starting checkSwitches")
local currentTime = os.time()
local nextWakeupTime = FAR_FUTURE_TIME
removeNextWakeup()
-- loop through all the smart switches, performing actions based
-- on their timeout values
for switchId, state in pairs(g_switches) do
-- get the device id for this smart switch
local smartSwitchId = state.smartSwitchId
local wakeupTime = checkSwitch(currentTime, smartSwitchId)
-- keep track of the earliest timeout of all switches that haven't timed out
if (wakeupTime < nextWakeupTime) then
nextWakeupTime = wakeupTime
end
end
log.debugValues ("Done with checkSwitches","nextWakeupTime", os.date(DATE_FORMAT, nextWakeupTime))
scheduleNextWakeup(nextWakeupTime)
end
-------------------------------------
----- SENSOR ADD / REMOVE LOGIC -----
-------------------------------------
local function initSensorState (sensorId)
log.debug ("Initializing sensor state", "sensorId", sensorId)
g_sensors[sensorId] = {
switches = {}
}
end
local function addSensor(sensorId, switchId)
log.debug ("addSensor","sensorId", sensorId, "switchId", switchId)
if (not g_sensors[sensorId]) then
initSensorState (sensorId)
end
g_sensors[sensorId].switches[switchId] = 1
g_switches[switchId].sensors[sensorId] = 1
end
local function removeSensor(sensorId, switchId)
log.debug ("removeSensor","sensorId", sensorId, "switchId", switchId)
g_sensors[sensorId].switches[switchId] = nil
g_switches[switchId].sensors[sensorId] = nil
end
------------------------------------------
-------- RUN / JOB HANDLERS --------------
------------------------------------------
local function setOnLevel(smartSwitchId, level)
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "OnLevel", level, smartSwitchId)
updateSwitchLevel(smartSwitchId)
end
local function setOffLevel(smartSwitchId, level)
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "OffLevel", level, smartSwitchId)
updateSwitchLevel(smartSwitchId)
end
local function setAutoTimeout(smartSwitchId, timeout)
local switchId = g_smartSwitches[smartSwitchId].switchId
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "AutoTimeout", timeout, smartSwitchId)
updateSwitchTimeout(switchId)
end
local function setManualTimeout(smartSwitchId, timeout)
local switchId = g_smartSwitches[smartSwitchId].switchId
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "ManualTimeout", timeout, smartSwitchId)
updateSwitchTimeout(switchId)
end
local function setLevel(smartSwitchId, level)
local switchId = g_smartSwitches[smartSwitchId].switchId
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Mode", MODE.MANUAL, smartSwitchId)
setSwitchLevel(switchId, level)
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Level", level, smartSwitchId)
updateSwitchTimeout(switchId)
end
local function setRememberManualLevel(smartSwitchId, rememberManualLevel)
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "RememberManualLevel", rememberManualLevel, smartSwitchId)
end
-- function to handle UPnP api calls
local function dispatchRun(lul_device, lul_settings, serviceId, action)
log.infoValues ("Entering dispatchRun", "lul_device", lul_device, "serviceId" , serviceId , "action" , action ,
"lul_settings" , (lul_settings))
local success = true
local lul_device = tonumber(lul_device)
if (serviceId == SID.SMART_SWITCH_CONTROLLER) then
if (action == "SetLevel") then
setLevel(lul_device, tonumber(lul_settings.NewLevel))
elseif (action == "SetOnLevel") then
setOnLevel(lul_device, tonumber(lul_settings.NewLevel))
elseif (action == "SetOffLevel") then
setOffLevel(lul_device, tonumber(lul_settings.NewLevel))
elseif (action == "SetAutoTimeout") then
setAutoTimeout(lul_device, tonumber(lul_settings.NewTimeout))
elseif (action == "SetManualTimeout") then
setManualTimeout(lul_device, tonumber(lul_settings.NewTimeout))
elseif (action == "SetRememberManualLevel") then
setRememberManualLevel(lul_device, toboolean(lul_settings.NewRememberManualLevel))
else
log.error("Unrecognized job request")
end
else
log.error("Unrecognized job request")
end
return (success)
end
----------------------------------------------
-------- CALLBACK HELPER FUNCTIONS -----------
----------------------------------------------
local function convertSwitchLevel(lul_variable, lul_value_new)
local newLevel = nil
if (lul_variable == "LoadLevelStatus") then
newLevel = tonumber(lul_value_new)
elseif (lul_variable == "Status") then
if (lul_value_new == "0") then
newLevel = 0
else
newLevel = 100
end
end
return newLevel
end
local function recordManualActivation(smartSwitchId, newLevel)
local switchId = g_smartSwitches[smartSwitchId].switchId
local manualTimeout = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "ManualTimeout", smartSwitchId, util.T_NUMBER)
-- only change to "manual" mode if there is a manualTimeout set
if (manualTimeout > 0) then
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Mode", MODE.MANUAL, smartSwitchId)
end
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Level", newLevel, smartSwitchId)
if (util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "RememberManualLevel", smartSwitchId, util.T_BOOLEAN)) then
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "OnLevel", newLevel, smartSwitchId)
end
updateSwitchTimeout(switchId)
end
local function sensorTripped(sensorId)
log.infoValues ("Sensor tripped", "sensorId", sensorId);
for switchId in pairs(g_sensors[sensorId].switches) do
local smartSwitchId = g_switches[switchId].smartSwitchId
local autoTimeout = util.getLuupVariable(
SID.SMART_SWITCH_CONTROLLER, "AutoTimeout",smartSwitchId, util.T_NUMBER)
local currentMode = util.getLuupVariable(
SID.SMART_SWITCH_CONTROLLER, "Mode",smartSwitchId, util.T_STRING)
-- only change to "AUTO" mode if the switch is "OFF" and there is an autoTimeout value
if (currentMode == MODE.OFF and autoTimeout > 0) then
setSwitchLevel(switchId, util.getLuupVariable(
SID.SMART_SWITCH_CONTROLLER, "OnLevel", smartSwitchId, util.T_NUMBER))
util.setLuupVariable(
SID.SMART_SWITCH_CONTROLLER, "Mode", MODE.AUTO, smartSwitchId)
end
-- clear the current timeout
util.setLuupVariable(
SID.SMART_SWITCH_CONTROLLER, "Timeout", FAR_FUTURE_TIME, smartSwitchId)
showStatusOnUI(smartSwitchId)
end
end
local function sensorReset(sensorId)
log.infoValues ("Sensor reset", "sensorId", sensorId);
for switchId in pairs(g_sensors[sensorId].switches) do
updateSwitchTimeout(switchId)
end
end
------------------------------------------------------
-------- VARIABLE WATCH CALLBACK HANDLERS ------------
------------------------------------------------------
-- These callback functions listen for changes in the state of the "target" devices
function sensorCallback(lul_device, lul_service, lul_variable, lul_value_old, lul_value_new)
log.debugValues("Entering sensorCallback", "lul_device", lul_device,
"lul_service", lul_service,
"lul_variable", lul_variable,
"lul_value_old", lul_value_old,
"lul_value_new", lul_value_new)
lul_device = tonumber(lul_device)
if (not g_sensors[lul_device]) then
log.debug("Not a sensor we care about")
return
end
if (lul_variable == "Tripped") then
if (lul_value_new == "1" and lul_value_old == "0") then
sensorTripped(lul_device)
elseif (lul_value_new == "0" and lul_value_old == "1") then
sensorReset(lul_device)
end
end
end
function switchCallback(lul_device, lul_service, lul_variable, lul_value_old, lul_value_new)
log.debugValues("switchCallback", "lul_device", lul_device,
"lul_service", lul_service,
"lul_variable", lul_variable,
"lul_value_old", lul_value_old,
"lul_value_new", lul_value_new)
local lul_device = tonumber(lul_device)
-- If this is a dimmer, ignore "Status" (we look at LoadLevelStatus instead)
if (lul_variable == "Status" and luup.device_supports_service(SID.DIMMER, lul_device)) then
log.debug("Ignoring change in Status for dimmer")
return
end
local newLevel = convertSwitchLevel(lul_variable, lul_value_new)
-- Check to see if this is a switch that we recognize / care about
if (g_switches[lul_device]) then
local smartSwitchId = g_switches[lul_device].smartSwitchId
local currentLevel = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "Level", smartSwitchId, util.T_NUMBER)
-- Check to see if we received a level in the callback that doesn't match our current level.
if (newLevel ~= nil and newLevel ~= currentLevel) then
log.infoValues ("Received manual override for switch","lul_device", lul_device, "smartSwitchId", smartSwitchId,
"currentLevel", currentLevel, "newLevel", newLevel)
recordManualActivation(smartSwitchId, newLevel)
else
log.debug ("received unchanged state in switch callback")
end
end
-- if this switch is registered as a "sensor", process accordingly
if (g_sensors[lul_device]) then
if (newLevel > 0) then
sensorTripped(lul_device)
else
sensorReset(lul_device)
end
end
end
local function setLogConfig(newLogConfig)
logConfig = newLogConfig
end
-----------------------------------
-------- INITIALIZATION -----------
-----------------------------------
--- init Luup variables if they don't have values
local function initLuupVariables()
util.initVariableIfNotSet(SID.SMART_SWITCH, "SwitchIds", "[]", g_deviceId)
util.initVariableIfNotSet(SID.SMART_SWITCH, "SwitchNames", "[]", g_deviceId) -- 2021.2.36 @akbooer
util.initVariableIfNotSet(SID.SMART_SWITCH, "UseSwitchNames", "0", g_deviceId) -- 2021.2.36 @akbooer
end
local function getDefaultParameters()
return
SID.SMART_SWITCH_CONTROLLER..",Level=0\n"..
SID.SMART_SWITCH_CONTROLLER..",Mode=" .. MODE.OFF .. "\n"..
SID.SMART_SWITCH_CONTROLLER..",Timeout=" .. FAR_FUTURE_TIME .. "\n"..
SID.SMART_SWITCH_CONTROLLER..",LastPollTime=0\n"..
SID.SMART_SWITCH_CONTROLLER..",OnLevel=100\n"..
SID.SMART_SWITCH_CONTROLLER..",OffLevel=0\n"..
SID.SMART_SWITCH_CONTROLLER..",AutoTimeout=300\n"..
SID.SMART_SWITCH_CONTROLLER..",ManualTimeout=1800\n"..
SID.SMART_SWITCH_CONTROLLER..",SensorIds=[]\n"..
SID.SMART_SWITCH_CONTROLLER..",RememberManualLevel=0"
end
-----------------------------------
-- 2021.02.27 @akbooer
-- write (possibly new) list of ids
local function updateIdsFromNames (NameVar, devNo, svcId, idVar)
local use_names = util.getLuupVariable(SID.SMART_SWITCH, "UseSwitchNames", g_deviceId, util.T_BOOLEAN)
if use_names then
local name2id = {}
for n, d in pairs(luup.devices) do -- make a map of all the device names/ids
name2id[d.description] = tostring(n)
end
local newIds = {}
local Names = util.getLuupVariable(svcId, NameVar, devNo, util.T_TABLE)
for _, name in ipairs(Names or {}) do -- make new list of ids from names
newIds[#newIds+1] = name2id[name]
end
util.setLuupVariable(svcId, idVar, newIds, devNo)
end
end
-- write (possibly new) list of names
local function updateNamesFromIds (NameVar, devNo, svcId, idVar)
local newNames = {}
local switchIds = util.getLuupVariable(svcId, idVar, devNo, util.T_TABLE)
for i, idstr in ipairs (switchIds) do
newNames[i] = (luup.devices[tonumber(idstr)] or {}) .description or '?'
end
util.setLuupVariable(svcId, NameVar, newNames, devNo)
end
-- callbacks to keep Switch and Sensor names in sync
-- variables: lul_device, lul_service, lul_variable, lul_value_old, lul_value_new
-- write (possibly new) list of SensorNames
function updateNamesFromSensorIds (...)
updateNamesFromIds ("SensorNames", ...)
end
-- write (possibly new) list of SwitchNames
function updateNamesFromSwitchIds (...)
updateNamesFromIds ("SwitchNames", ...)
end
-- 2021.02.27 @akbooer
-----------------------------------
local function initSwitchState(switchId, smartSwitchId)
log.debugValues ("Initializing switch state", "switchId", switchId, "smartSwitchId", smartSwitchId)
g_switches[switchId] = {
smartSwitchId = smartSwitchId,
sensors = {}
}
end
local function initSmartSwitch(smartSwitchId)
local switchId = tonumber(luup.devices[smartSwitchId].id)
log.debugValues ("Initializing smart switch", "switchId", switchId, "smartSwitchId", smartSwitchId)
initSwitchState(switchId, smartSwitchId)
updateIdsFromNames ("SensorNames", smartSwitchId, SID.SMART_SWITCH_CONTROLLER, "SensorIds") -- 2021.02.27 @akbooer
local sensorIds = util.getLuupVariable(SID.SMART_SWITCH_CONTROLLER, "SensorIds", smartSwitchId, util.T_TABLE)
local validSensorIds = {}
for index, sensorId in pairs(sensorIds) do
if (luup.devices[tonumber(sensorId)] ~= nil) then
table.insert(validSensorIds, sensorId)
addSensor(tonumber(sensorId), switchId)
end
end
if (#sensorIds ~= #validSensorIds) then
log.error ("Found invalid sensor id in sensor list for switch ",smartSwitchId," - old list: ", sensorIds, ", new list: ", validSensorIds)
util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "SensorIds", validSensorIds, smartSwitchId)
end
g_smartSwitches[smartSwitchId] = { ["switchId"] = switchId }
updateNamesFromSensorIds (smartSwitchId, SID.SMART_SWITCH_CONTROLLER, "SensorIds") -- 2021.02.27 @akbooer
-- Clear out old "StatusText" variable
--util.setLuupVariable(SID.SMART_SWITCH_CONTROLLER, "StatusText", "", smartSwitchId)
showStatusOnUI(smartSwitchId)
end
local function initSmartSwitches()
log.info ("Finding and initializing smart switch devices for parent deviceId = ", g_deviceId)
for deviceId, deviceData in pairs(luup.devices) do
log.debug ("examining deviceId = ", deviceId)
if (deviceData.device_num_parent == g_deviceId and
deviceData.device_type == DID_SMART_SWITCH_CONTROLLER) then
log.info ("found child SmartSwitchController device, deviceId = ", deviceId)
initSmartSwitch(deviceId)
end
end
-- Clear out old "StatusText" variable
util.setLuupVariable(SID.SMART_SWITCH, "StatusText", "", g_deviceId)
log.debug ("done with state initialization")
log.debugValues ("", "g_switches", g_switches)
log.debugValues ("", "g_sensors", g_sensors)
log.debugValues ("", "g_smartSwitches", g_smartSwitches)
end
-- Synchronize the Smart Switch Controller devices
local function syncChildDevices()
updateIdsFromNames ("SwitchNames", g_deviceId, SID.SMART_SWITCH, "SwitchIds") -- 2021.02.26 @akbooer
local switchIds = util.getLuupVariable(SID.SMART_SWITCH, "SwitchIds", g_deviceId, util.T_TABLE)
log.debugValues ("", "switchIds", switchIds)
local validSwitchIds = {}
local rootPtr = luup.chdev.start(g_deviceId)
for index, switchIdStr in pairs(switchIds) do
local switchId = tonumber(switchIdStr)
if (luup.devices[switchId] ~= nil) then
log.debugValues ("syncing", "switchId", switchId)
table.insert(validSwitchIds, switchIdStr)
local description = "SS: " .. luup.devices[switchId].description
luup.chdev.append(g_deviceId, rootPtr,
tostring(switchId), description,
nil,
"D_SmartSwitchController1.xml", "", getDefaultParameters(), false)
end
end
if (#switchIds ~= #validSwitchIds) then
log.error ("Found invalid switch id in switch list, old list: ", switchIds, ", new list: ", validSwitchIds)
util.setLuupVariable(SID.SMART_SWITCH, "SwitchIds", validSwitchIds, g_deviceId)
end
updateNamesFromSwitchIds (g_deviceId, SID.SMART_SWITCH, "SwitchIds") -- 2021.02.26 @akbooer
luup.chdev.sync(g_deviceId, rootPtr)
end
-- Register with ALTUI once it is ready
local function _registerWithALTUI()
for deviceId, device in pairs( luup.devices ) do
if ( device.device_type == "urn:schemas-upnp-org:device:altui:1" ) then
if luup.is_ready( deviceId ) then
log.info( "Register with ALTUI main device #" .. tostring( deviceId ), g_deviceId )
luup.call_action(
"urn:upnp-org:serviceId:altui1",
"RegisterPlugin",
{
newDeviceType = "urn:schemas-hugheaves-com:device:SmartSwitch:1",
newScriptFile = "J_SmartSwitch1.js",
newDeviceDrawFunc = "SmartSwitch.drawDevice"
},
deviceId
)
luup.call_action(
"urn:upnp-org:serviceId:altui1",
"RegisterPlugin",
{
newDeviceType = "urn:schemas-hugheaves-com:device:SmartSwitchController:1",
newScriptFile = "J_SmartSwitchController1.js",
newDeviceDrawFunc = "SmartSwitchController.drawDevice"
},
deviceId
)
else
log.info( "ALTUI main device #" .. tostring( deviceId ) .. " is not yet ready, retry to register in 10 seconds...", g_deviceId )
luup.call_delay( "ZiGateGateway.registerWithALTUI", 10 )
end
break
end
end
end
local function initialize(lul_device)
local success = false
local errorMsg = nil
g_deviceId = tonumber(lul_device)
util.initLogging(LOG_PREFIX, logConfig, SID.SMART_SWITCH, g_deviceId)
log.info ("Initializing SmartSwitch plugin for device " , g_deviceId)
--
-- log.error ("luup.devices = " , luup.devices)
-- set plugin version number
luup.variable_set(SID.SMART_SWITCH, "PluginVersion", PLUGIN_VERSION, g_deviceId)
initLuupVariables()
syncChildDevices()
initSmartSwitches()
luup.variable_watch("switchCallback", SID.SWITCH, "Status", nil)
luup.variable_watch("switchCallback", SID.DIMMER, "Status", nil)
luup.variable_watch("switchCallback", SID.DIMMER, "LoadLevelStatus", nil)
luup.variable_watch("sensorCallback", SID.SECURITY_SENSOR, "Tripped", nil)
luup.variable_watch("updateNamesFromSwitchIds", SID.SMART_SWITCH, "SwitchIds", g_deviceId) -- 2021.02.26 @akbooer
luup.variable_watch("updateNamesFromSensorIds", SID.SMART_SWITCH_CONTROLLER, "SensorIds", nil) -- 2021.02.27 @akbooer
-- Register with ALTUI
luup.call_delay( "SmartSwitch.registerWithALTUI", 10 )
luup.set_failure(0, lul_device)
log.info("Done with initialization")
return success, errorMsg, "SmartSwitch"
end
-- Promote the functions used by Vera's luup.xxx functions to the global name space
_G["SmartSwitch.registerWithALTUI"] = _registerWithALTUI
-- RETURN GLOBAL FUNCTIONS
return {
initialize=initialize,
dispatchRun=dispatchRun,
checkSwitches=checkSwitches,
sensorCallback=sensorCallback,
switchCallback=switchCallback,
setLogConfig=setLogConfig
}