1036 lines
32 KiB
Lua
1036 lines
32 KiB
Lua
--[[
|
|
|
|
|
|
|
|
bibliothèque de fonctions pour domoticz
|
|
utiles à la réalisation de scripts d'automation en langage lua
|
|
|
|
/!\ certaines fonctions ne fonctionneront pas sous windows.
|
|
|
|
copier ce qui se trouve entre les 2 lignes ci dessous, en début de tout vos script
|
|
pour charger ce fichier et pouvoir en utiliser les fonctions
|
|
|
|
|
|
--------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
-- chargement des modules (http://easydomoticz.com/forum/viewtopic.php?f=17&t=3940)
|
|
dofile('/home/pi/domoticz/scripts/lua/modules.lua')
|
|
|
|
local debug = true -- true pour voir les logs dans la console log Dz ou false pour ne pas les voir
|
|
|
|
|
|
--------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
]]
|
|
|
|
|
|
--------------------------------
|
|
------ USER SETTINGS ------
|
|
--------------------------------
|
|
|
|
-- domoticz
|
|
domoticzIP = '192.168.22.100' --'127.0.0.1'
|
|
domoticzPORT = '8080'
|
|
domoticzUSER = '' -- nom d'utilisateur
|
|
domoticzPSWD = '' -- mot de pass
|
|
domoticzPASSCODE = '' -- pour interrupteur protégés
|
|
domoticzURL = 'http://'..domoticzIP..':'..domoticzPORT
|
|
|
|
|
|
|
|
--------------------------------
|
|
------ END ------
|
|
--------------------------------
|
|
|
|
|
|
-- chemin vers le dossier lua et curl
|
|
if (package.config:sub(1,1) == '/') then
|
|
-- system linux
|
|
luaDir = debug.getinfo(1).source:match("@?(.*/)")
|
|
curl = '/usr/bin/curl -m 15 ' -- ne pas oublier l'espace à la fin
|
|
else
|
|
-- system windows
|
|
luaDir = string.gsub(debug.getinfo(1).source:match("@?(.*\\)"),'\\','\\\\')
|
|
-- download curl : https://bintray.com/vszakats/generic/download_file?file_path=curl-7.54.0-win32-mingw.7z
|
|
curl = 'c:\\Programs\\Curl\\curl.exe ' -- ne pas oublier l'espace à la fin
|
|
end
|
|
|
|
-- chargement du fichier JSON.lua
|
|
json = assert(loadfile(luaDir..'JSON.lua'))()
|
|
|
|
--time.hour ou time.min ou time.sec
|
|
--ex : if (time.hour == 17 and time.min == 05) then
|
|
time = os.date("*t")
|
|
|
|
-- retourne l'heure actuelle ex: "12:45"
|
|
heure = string.sub(os.date("%X"), 1, 5)
|
|
|
|
-- retourne la date ex: "01:01"
|
|
date = os.date("%d:%m")
|
|
|
|
-- retourne l'heure du lever de soleil ex: "06:41"
|
|
leverSoleil = string.sub(os.date("!%X",60*timeofday['SunriseInMinutes']), 1, 5)
|
|
|
|
-- retourne l'heure du coucher de soleil ex: "22:15"
|
|
coucherSoleil = string.sub(os.date("!%X",60*timeofday['SunsetInMinutes']), 1, 5)
|
|
|
|
-- retourne le jour actuel en français ex: "mardi"
|
|
days = {"dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"}
|
|
jour = days[(os.date("%w")+1)]
|
|
|
|
-- retourne VRAI si la semaine est paire
|
|
-- usage :
|
|
-- if semainePaire() then ..
|
|
function semainePaire()
|
|
local tm = os.time()
|
|
local function getYearBeginDayOfWeek()
|
|
yearBegin = os.time{year=os.date("*t",tm).year,month=1,day=1}
|
|
yearBeginDayOfWeek = tonumber(os.date("%w",yearBegin))
|
|
-- sunday correct from 0 -> 7
|
|
if(yearBeginDayOfWeek == 0) then yearBeginDayOfWeek = 7 end
|
|
return yearBeginDayOfWeek
|
|
end
|
|
local function getDayAdd()
|
|
yearBeginDayOfWeek = getYearBeginDayOfWeek(tm)
|
|
if(yearBeginDayOfWeek < 5 ) then
|
|
-- first day is week 1
|
|
dayAdd = (yearBeginDayOfWeek - 2)
|
|
else
|
|
-- first day is week 52 or 53
|
|
dayAdd = (yearBeginDayOfWeek - 9)
|
|
end
|
|
return dayAdd
|
|
end
|
|
dayOfYear = os.date("%j",tm)
|
|
dayAdd = getDayAdd(tm)
|
|
dayOfYearCorrected = dayOfYear + dayAdd
|
|
if(dayOfYearCorrected < 0) then
|
|
-- week of last year - decide if 52 or 53
|
|
lastYearBegin = os.time{year=os.date("*t",tm).year-1,month=1,day=1}
|
|
lastYearEnd = os.time{year=os.date("*t",tm).year-1,month=12,day=31}
|
|
dayAdd = getDayAdd(lastYearBegin)
|
|
dayOfYear = dayOfYear + os.date("%j",lastYearEnd)
|
|
dayOfYearCorrected = dayOfYear + dayAdd
|
|
end
|
|
weekNum = math.floor((dayOfYearCorrected) / 7) + 1
|
|
if( (dayOfYearCorrected > 0) and weekNum == 53) then
|
|
-- check if it is not considered as part of week 1 of next year
|
|
nextYearBegin = os.time{year=os.date("*t",tm).year+1,month=1,day=1}
|
|
yearBeginDayOfWeek = getYearBeginDayOfWeek(nextYearBegin)
|
|
if(yearBeginDayOfWeek < 5 ) then
|
|
weekNum = 1
|
|
end
|
|
end
|
|
return weekNum%2 == 0
|
|
end
|
|
|
|
|
|
-- il fait jour
|
|
dayTime = timeofday['Daytime']
|
|
-- il fait nuit
|
|
nightTime = timeofday['Nighttime']
|
|
|
|
-- température
|
|
function getTemp(device)
|
|
return round(tonumber(otherdevices_temperature[device]),1)
|
|
end
|
|
|
|
-- humidité
|
|
function getHum(device)
|
|
return round(tonumber(otherdevices_humidity[device]),1)
|
|
end
|
|
|
|
-- humidité moyenne
|
|
function humMoy(device)
|
|
local monthLog = assert(io.popen(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..'/json.htm?range=month&sensor=temp&type=graph&idx='..otherdevices_idx[device]..'"'))
|
|
local list = monthLog:read('*all')
|
|
monthLog:close()
|
|
local data = ReverseTable(json:decode(list).result)
|
|
return(round(data[1].hu))
|
|
end
|
|
|
|
-- humidité absolue
|
|
function humAbs(t,hr)
|
|
-- https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
|
|
-- Formule pour calculer l'humidité absolue
|
|
-- Dans la formule ci-dessous, la température (T) est exprimée en degrés Celsius, l'humidité relative (hr) est exprimée en%, et e est la base des logarithmes naturels 2.71828 [élevée à la puissance du contenu des crochets]:
|
|
-- Humidité absolue (grammes / m3 ) = (6,122 * e^[(17,67 * T) / (T + 243,5)] * rh * 2,1674))/(273,15 + T)
|
|
-- Cette formule est précise à 0,1% près, dans la gamme de température de -30 ° C à + 35 ° C
|
|
return round((6.112 * math.exp((17.67 * t)/(t+243.5)) * hr * 2.1674)/ (273.15 + t),1)
|
|
end
|
|
|
|
-- set setpoint (faster way)
|
|
function setPoint(device,value)
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..'/json.htm?type=command¶m=udevice&idx='..otherdevices_idx[device]..'&nvalue=0&svalue='..value..'" &')
|
|
end
|
|
|
|
function dimUp15(device)
|
|
-- 15 step
|
|
switchOn(device, constrain(otherdevices_svalues[device]+1,1,15))
|
|
end
|
|
|
|
function dimDown15(device)
|
|
-- 15 step
|
|
switchOn(device, constrain(otherdevices_svalues[device]-1,1,15))
|
|
end
|
|
|
|
function dimUp(device)
|
|
-- 100 step
|
|
switchOn(device, constrain(otherdevices_svalues[device]+10,10,100))
|
|
end
|
|
|
|
function dimDown(device)
|
|
-- 100 step
|
|
switchOn(device, constrain(otherdevices_svalues[device]-10,10,100))
|
|
end
|
|
|
|
-- vérifie s'il y a eu changement d'état
|
|
function stateChange(device)
|
|
if (uservariables['lastState_'..device] == nil) then
|
|
creaVar('lastState_'..device,otherdevices[device])
|
|
log('stateChange : création variable manquante lastState_'..device,debug)
|
|
return false
|
|
elseif (devicechanged[device] == nil) then
|
|
return false
|
|
elseif (devicechanged[device] == uservariables['lastState_'..device]) then
|
|
return false
|
|
else
|
|
duree = lastSeen(uservariables_lastupdate['lastState_'..device])
|
|
updateVar('lastState_'..device,otherdevices[device])
|
|
return otherdevices[device]
|
|
end
|
|
end
|
|
|
|
-- convertion degrés en direction cardinale
|
|
function wind_cardinals(deg)
|
|
local cardinalDirections = {
|
|
['N'] = {348.75, 360},
|
|
['N'] = {0, 11.25},
|
|
['NNE'] = {11.25, 33.75},
|
|
['NE'] = {33.75, 56.25},
|
|
['ENE'] = {56.25, 78.75},
|
|
['E'] = {78.75, 101.25},
|
|
['ESE'] = {101.25, 123.75},
|
|
['SE'] = {123.75, 146.25},
|
|
['SSE'] = {146.25, 168.75},
|
|
['S'] = {168.75, 191.25},
|
|
['SSW'] = {191.25, 213.75},
|
|
['SW'] = {213.75, 236.25},
|
|
['WSW'] = {236.25, 258.75},
|
|
['W'] = {258.75, 281.25},
|
|
['WNW'] = {281.25, 303.75},
|
|
['NW'] = {303.75, 326.25},
|
|
['NNW'] = {326.25, 348.75}
|
|
}
|
|
local cardinal
|
|
for dir, angle in pairs(cardinalDirections) do
|
|
if (deg >= angle[1] and deg < angle[2]) then
|
|
cardinal = dir
|
|
break
|
|
end
|
|
end
|
|
return cardinal
|
|
end
|
|
|
|
-- dump all variables supplied to the script
|
|
-- usage
|
|
-- LogVariables(_G,0,'')
|
|
function LogVariables(x,depth,name)
|
|
for k,v in pairs(x) do
|
|
if (depth>0) or ((string.find(k,'device')~=nil) or (string.find(k,'variable')~=nil) or
|
|
(string.sub(k,1,4)=='time') or (string.sub(k,1,8)=='security')) then
|
|
if type(v)=="string" then print(name.."['"..k.."'] = '"..v.."'") end
|
|
if type(v)=="number" then print(name.."['"..k.."'] = "..v) end
|
|
if type(v)=="boolean" then print(name.."['"..k.."'] = "..tostring(v)) end
|
|
if type(v)=="table" then LogVariables(v,depth+1,k); end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- os.execute() output or web page content return
|
|
-- usage
|
|
-- local resultat = os.capture(cmd , true)
|
|
-- print('resultat: ' .. resultat)
|
|
function os.capture(cmd, raw)
|
|
local f = assert(io.popen(cmd, 'r'))
|
|
local s = assert(f:read('*a'))
|
|
f:close()
|
|
if raw then return s end
|
|
s = string.gsub(s, '^%s+', '')
|
|
s = string.gsub(s, '%s+$', '')
|
|
s = string.gsub(s, '[\n\r]+', ' ')
|
|
return s
|
|
end
|
|
|
|
-- retourne le type de la variable
|
|
-- 'string' , 'number' , 'table'
|
|
function typeof(var)
|
|
local _type = type(var);
|
|
if(_type ~= "table" and _type ~= "userdata") then
|
|
return _type;
|
|
end
|
|
local _meta = getmetatable(var);
|
|
if(_meta ~= nil and _meta._NAME ~= nil) then
|
|
return _meta._NAME;
|
|
else
|
|
return _type;
|
|
end
|
|
end
|
|
|
|
-- affiche les logs en bleu sauf si debug est spécifié à false
|
|
function log(txt,debug)
|
|
if (debug ~= false) then
|
|
--print("<font color='#0206a9'>"..txt.."</font>")
|
|
print(txt)
|
|
end
|
|
end
|
|
|
|
-- affiche les logs en rouge sauf si debug est spécifié à false
|
|
function warn(txt,debug)
|
|
if (debug ~= false) then
|
|
--print("<font color='red'>"..txt.."</font>")
|
|
print(txt)
|
|
end
|
|
end
|
|
|
|
-- écriture dans un fichier texte dans le dossier lua
|
|
function logToFile(fileName,data)
|
|
f = assert(io.open(luaDir..fileName..'.txt',"a"))
|
|
f:write(os.date("%c")..' '..data..'\n')
|
|
f:close()
|
|
end
|
|
|
|
-- teste l'existance d'un fichier
|
|
function file_exists(file)
|
|
local f = io.open(file, "rb")
|
|
if f then f:close() end
|
|
return f ~= nil
|
|
end
|
|
|
|
-- retourne le nom du switch selon son IDX
|
|
function getDeviceName(deviceIDX)
|
|
for i, v in pairs(otherdevices_idx) do
|
|
if v == deviceIDX then
|
|
return i
|
|
end
|
|
end
|
|
return 0
|
|
end
|
|
|
|
-- get all lines from a file, returns an empty
|
|
-- list/table if the file does not exist
|
|
function lines_from(file)
|
|
if not file_exists(luaDir..file..'.txt') then return {} end
|
|
lines = {}
|
|
for line in io.lines(luaDir..file..'.txt') do
|
|
lines[#lines + 1] = line
|
|
end
|
|
return lines
|
|
end
|
|
|
|
-- encode du texte pour le passer dans une url
|
|
function url_encode(str)
|
|
if (str) then
|
|
str = string.gsub (str, "\n", "\r\n")
|
|
str = string.gsub (str, "([^%w %-%_%.%~])",
|
|
function (c) return string.format ("%%%02X", string.byte(c)) end)
|
|
str = string.gsub (str, " ", "+")
|
|
end
|
|
return str
|
|
end
|
|
|
|
-- supprime les accents de la chaîne
|
|
function sans_accent(str)
|
|
if (str) then
|
|
str = string.gsub (str,"Ç", "C")
|
|
str = string.gsub (str,"ç", "c")
|
|
str = string.gsub (str,"[-èéêë']+", "e")
|
|
str = string.gsub (str,"[-ÈÉÊË']+", "E")
|
|
str = string.gsub (str,"[-àáâãäå']+", "a")
|
|
str = string.gsub (str,"[-@ÀÁÂÃÄÅ']+", "A")
|
|
str = string.gsub (str,"[-ìíîï']+", "i")
|
|
str = string.gsub (str,"[-ÌÍÎÏ']+", "I")
|
|
str = string.gsub (str,"[-ðòóôõö']+", "o")
|
|
str = string.gsub (str,"[-ÒÓÔÕÖ']+", "O")
|
|
str = string.gsub (str,"[-ùúûü']+", "u")
|
|
str = string.gsub (str,"[-ÙÚÛÜ']+", "U")
|
|
str = string.gsub (str,"[-ýÿ']+", "y")
|
|
str = string.gsub (str,"Ý", "Y")
|
|
end
|
|
return str
|
|
end
|
|
|
|
-- retourne le temps en seconde depuis la dernière maj du péréphérique
|
|
function lastSeen(device)
|
|
timestamp = otherdevices_lastupdate[device] or device
|
|
y, m, d, H, M, S = timestamp:match("(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)")
|
|
difference = os.difftime(os.time(), os.time{year=y, month=m, day=d, hour=H, min=M, sec=S})
|
|
return difference
|
|
end
|
|
|
|
-- contraindre
|
|
function constrain(x, a, b)
|
|
if (x < a) then
|
|
return a
|
|
elseif (x > b) then
|
|
return b
|
|
else
|
|
return x
|
|
end
|
|
end
|
|
|
|
-- arrondire
|
|
function round(num, dec)
|
|
if num == 0 then
|
|
return 0
|
|
else
|
|
local mult = 10^(dec or 0)
|
|
return math.floor(num * mult + 0.5) / mult
|
|
end
|
|
end
|
|
|
|
-- met le script en pause (fortement déconseillé)
|
|
-- usage
|
|
-- sleep(10) -- pour mettre en pause 10 secondes
|
|
function sleep(n)
|
|
os.execute('sleep '..n)
|
|
end
|
|
|
|
-- création de variable utilisateur
|
|
-- usage
|
|
-- creaVar('toto','10') -- pour créer une variable nommée toto comprenant la valeur 10
|
|
function creaVar(name,value)
|
|
local api = '/json.htm?type=command¶m=adduservariable'
|
|
local name = '&vname='..url_encode(name)
|
|
local vtype = '&vtype=2'
|
|
local value = '&vvalue='..url_encode(value)
|
|
api = api..name..vtype..value
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..api..'" &')
|
|
end
|
|
|
|
-- update an existing variable
|
|
function updateVar(name,value)
|
|
local api = '/json.htm?type=command¶m=updateuservariable'
|
|
local name = '&vname='..url_encode(name)
|
|
local vtype = '&vtype=2'
|
|
local value = '&vvalue='..url_encode(value)
|
|
api = api..name..vtype..value
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..api..'" &')
|
|
end
|
|
|
|
-- envoie dans un capteur text une chaîne de caractères
|
|
-- le text sera intercepté et lu par la custom page grâce à sa fonction MQTT
|
|
-- usage
|
|
-- speak('tts','bonjour nous sommes dimanche')
|
|
function speak(TTSDeviceName,txt)
|
|
commandArray[#commandArray+1] = {['OpenURL'] = domoticzIP..":"..domoticzPORT..'/json.htm?type=command¶m=udevice&idx='..otherdevices_idx[TTSDeviceName]..'&nvalue=0&svalue='..url_encode(txt)}
|
|
end
|
|
|
|
-- récupère les infos json du périphérique
|
|
-- usage
|
|
-- local lampe = jsonInfos('ma lampe')
|
|
-- print(lampe.Name)
|
|
-- print(lampe.Status)
|
|
-- etc..
|
|
function jsonInfos(device)
|
|
local rid = assert(io.popen(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..'/json.htm?type=devices&rid='..otherdevices_idx[device]..'"'))
|
|
local list = rid:read('*all')
|
|
rid:close()
|
|
return json:decode(list).result[1]
|
|
end
|
|
|
|
-- parcours la table dans l'ordre
|
|
function spairs(t)
|
|
local keys = {}
|
|
for k in pairs(t) do keys[#keys+1] = k end
|
|
table.sort(keys)
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
if keys[i] then
|
|
return keys[i], t[keys[i]]
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Renverse une table
|
|
function ReverseTable(t)
|
|
local reversedTable = {}
|
|
local itemCount = #t
|
|
for k, v in ipairs(t) do
|
|
reversedTable[itemCount + 1 - k] = v
|
|
end
|
|
return reversedTable
|
|
end
|
|
|
|
-- affiche le contenu d'une table
|
|
--[[
|
|
|
|
Author: Julio Manuel Fernandez-Diaz
|
|
Date: January 12, 2007
|
|
(For Lua 5.1)
|
|
|
|
Modified slightly by RiciLake to avoid the unnecessary table traversal in tablecount()
|
|
|
|
Formats tables with cycles recursively to any depth.
|
|
The output is returned as a string.
|
|
References to other tables are shown as values.
|
|
Self references are indicated.
|
|
|
|
The string returned is "Lua code", which can be procesed
|
|
(in the case in which indent is composed by spaces or "--").
|
|
Userdata and function keys and values are shown as strings,
|
|
which logically are exactly not equivalent to the original code.
|
|
|
|
This routine can serve for pretty formating tables with
|
|
proper indentations, apart from printing them:
|
|
|
|
print(table_show(t, "t")) -- a typical use
|
|
|
|
Heavily based on "Saving tables with cycles", PIL2, p. 113.
|
|
|
|
Arguments:
|
|
t is the table.
|
|
name is the name of the table (optional)
|
|
indent is a first indentation (optional).
|
|
]]
|
|
function table_show(t, name, indent)
|
|
local cart -- a container
|
|
local autoref -- for self references
|
|
|
|
--[[ counts the number of elements in a table
|
|
local function tablecount(t)
|
|
local n = 0
|
|
for _, _ in pairs(t) do n = n+1 end
|
|
return n
|
|
end
|
|
]]
|
|
-- (RiciLake) returns true if the table is empty
|
|
local function isemptytable(t) return next(t) == nil end
|
|
|
|
local function basicSerialize (o)
|
|
local so = tostring(o)
|
|
if type(o) == "function" then
|
|
local info = debug.getinfo(o, "S")
|
|
-- info.name is nil because o is not a calling level
|
|
if info.what == "C" then
|
|
return string.format("%q", so .. ", C function")
|
|
else
|
|
-- the information is defined through lines
|
|
return string.format("%q", so .. ", defined in (" ..
|
|
info.linedefined .. "-" .. info.lastlinedefined ..
|
|
")" .. info.source)
|
|
end
|
|
elseif type(o) == "number" or type(o) == "boolean" then
|
|
return so
|
|
else
|
|
return string.format("%q", so)
|
|
end
|
|
end
|
|
|
|
local function addtocart (value, name, indent, saved, field)
|
|
indent = indent or ""
|
|
saved = saved or {}
|
|
field = field or name
|
|
|
|
cart = cart .. indent .. field
|
|
|
|
if type(value) ~= "table" then
|
|
cart = cart .. " = " .. basicSerialize(value) .. ";\n"
|
|
else
|
|
if saved[value] then
|
|
cart = cart .. " = {}; -- " .. saved[value]
|
|
.. " (self reference)\n"
|
|
autoref = autoref .. name .. " = " .. saved[value] .. ";\n"
|
|
else
|
|
saved[value] = name
|
|
--if tablecount(value) == 0 then
|
|
if isemptytable(value) then
|
|
cart = cart .. " = {};\n"
|
|
else
|
|
cart = cart .. " = {\n"
|
|
for k, v in pairs(value) do
|
|
k = basicSerialize(k)
|
|
local fname = string.format("%s[%s]", name, k)
|
|
field = string.format("[%s]", k)
|
|
-- three spaces between levels
|
|
addtocart(v, fname, indent .. " ", saved, field)
|
|
end
|
|
cart = cart .. indent .. "};\n"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
name = name or "table"
|
|
if type(t) ~= "table" then
|
|
return name .. " = " .. basicSerialize(t)
|
|
end
|
|
cart, autoref = "", ""
|
|
addtocart(t, name, indent)
|
|
return cart .. autoref
|
|
end
|
|
|
|
-- retourne la table des derniers log (première ligne = dernier log)
|
|
function lastLogEntry()
|
|
local rid = assert(io.popen(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..'/json.htm?type=command¶m=getlog"'))
|
|
local list = rid:read('*all')
|
|
rid:close()
|
|
local tableau = json:decode(list).result
|
|
return ReverseTable(tableau)
|
|
end
|
|
|
|
-- notification pushbullet
|
|
-- usage:
|
|
-- pushbullet('test','ceci est un message test')
|
|
function pushbullet(title,body)
|
|
local settings = assert(io.popen(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..'/json.htm?type=settings"'))
|
|
local list = settings:read('*all')
|
|
settings:close()
|
|
local pushbullet_key = json:decode(list).PushbulletAPI
|
|
os.execute(curl..'-H \'Access-Token:'..pushbullet_key..'\' -H \'Content-Type:application/json\' --data-binary \'{"title":"'..title..'","body":"'..body..'","type":"note"}\' -X POST "https://api.pushbullet.com/v2/pushes"')
|
|
end
|
|
|
|
-- switch On a device and set level if dimmmable
|
|
function switchOn(device,level)
|
|
local api = '/json.htm?type=command¶m=switchlight'
|
|
local idx = '&idx='..otherdevices_idx[device]
|
|
local cmd
|
|
if level ~= nil then
|
|
cmd = '&switchcmd=Set%20Level&level='..level
|
|
else
|
|
cmd = '&switchcmd=On'
|
|
end
|
|
local passcode = '&passcode='..domoticzPASSCODE
|
|
api = api..idx..cmd..passcode
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..api..'" &')
|
|
end
|
|
|
|
-- switch On a devive for x secondes
|
|
function switchOnFor(device, secs)
|
|
switchOn(device)
|
|
commandArray[#commandArray+1] = {[device] = "Off AFTER "..secs}
|
|
end
|
|
|
|
-- switch Off a device
|
|
function switchOff(device)
|
|
local api = '/json.htm?type=command¶m=switchlight'
|
|
local idx = '&idx='..otherdevices_idx[device]
|
|
local cmd = '&switchcmd=Off'
|
|
local passcode = '&passcode='..domoticzPASSCODE
|
|
api = api..idx..cmd..passcode
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..api..'" &')
|
|
end
|
|
|
|
-- Toggle a device
|
|
function switch(device)
|
|
local api = '/json.htm?type=command¶m=switchlight'
|
|
local idx = '&idx='..otherdevices_idx[device]
|
|
local cmd = '&switchcmd=Toggle'
|
|
local passcode = '&passcode='..domoticzPASSCODE
|
|
api = api..idx..cmd..passcode
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..api..'" &')
|
|
end
|
|
|
|
-- switch On a group or scene
|
|
function groupOn(device)
|
|
local api = '/json.htm?type=command¶m=switchscene'
|
|
local idx = '&idx='..otherdevices_scenesgroups_idx[device]
|
|
local cmd = '&switchcmd=On'
|
|
local passcode = '&passcode='..domoticzPASSCODE
|
|
api = api..idx..cmd..passcode
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..api..'" &')
|
|
end
|
|
|
|
-- switch Off a group
|
|
function groupOff(device)
|
|
local api = '/json.htm?type=command¶m=switchscene'
|
|
local idx = '&idx='..otherdevices_scenesgroups_idx[device]
|
|
local cmd = '&switchcmd=Off'
|
|
local passcode = '&passcode='..domoticzPASSCODE
|
|
api = api..idx..cmd..passcode
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..api..'" &')
|
|
end
|
|
|
|
-- Set switch to Stop
|
|
function switchStop(device)
|
|
local api = '/json.htm?type=command¶m=switchlight'
|
|
local idx = '&idx='..otherdevices_idx[device]
|
|
local cmd = '&switchcmd=Stop'
|
|
local passcode = '&passcode='..domoticzPASSCODE
|
|
api = api..idx..cmd..passcode
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..api..'" &')
|
|
end
|
|
|
|
-- Setup a color & brightness of an RGB(W) light
|
|
-- API : https://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Set_an_RGB.28W.29_light_to_a_certain_color_and_brightness
|
|
function setColorAndBrightness(device, color, brightness)
|
|
local api = '/json.htm?type=command¶m=setcolbrightnessvalue'
|
|
local idx = '&idx='..otherdevices_idx[device]
|
|
--local color = '&hue='..color
|
|
local color = '&hex='..color
|
|
local brightness = '&brightness='..brightness
|
|
local iswhite = '&iswhite=false'
|
|
local passcode = '&passcode='..domoticzPASSCODE
|
|
api = api..idx..color..brightness..iswhite..passcode
|
|
os.execute(curl..'-u '..domoticzUSER..':'..domoticzPSWD..' "'..domoticzURL..api..'" &')
|
|
end
|
|
|
|
function KelvinToRGB(temp)
|
|
-- http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
|
|
temp = temp/100
|
|
local red, green, blue
|
|
--Calculate Red:
|
|
if temp <= 66 then
|
|
red = 255
|
|
else
|
|
red = constrain(round(329.698727446 * ((temp - 60) ^ -0.1332047592)),0,255)
|
|
end
|
|
--Calculate Green:
|
|
if temp <= 66 then
|
|
green = constrain(round(99.4708025861 * math.log(temp) - 161.1195681661),0,255)
|
|
else
|
|
green = constrain(round(288.1221695283 * ((temp - 60) ^ -0.0755148492)),0,255)
|
|
end
|
|
--Calculate Blue:
|
|
if temp >= 66 then
|
|
blue = 255
|
|
else
|
|
if temp <= 19 then
|
|
blue = 0
|
|
else
|
|
blue = constrain(round(138.5177312231 * math.log(temp - 10) - 305.0447927307),0,255)
|
|
end
|
|
end
|
|
return {red,green,blue}
|
|
end
|
|
|
|
function RGBToHex(rgb)
|
|
-- https://gist.github.com/marceloCodget/3862929
|
|
local hexadecimal = ''
|
|
for key, value in pairs(rgb) do
|
|
local hex = ''
|
|
while(value > 0)do
|
|
local index = math.fmod(value, 16) + 1
|
|
value = math.floor(value / 16)
|
|
hex = string.sub('0123456789ABCDEF', index, index) .. hex
|
|
end
|
|
if(string.len(hex) == 0)then
|
|
hex = '00'
|
|
elseif(string.len(hex) == 1)then
|
|
hex = '0' .. hex
|
|
end
|
|
hexadecimal = hexadecimal .. hex
|
|
end
|
|
return hexadecimal
|
|
end
|
|
|
|
function suntimeToKelvin()
|
|
-- http://easydomoticz.com/forum/viewtopic.php?f=10&t=6160
|
|
local mini = 1900
|
|
local maxi = 6600
|
|
local delta = maxi - mini
|
|
local wakeup = 60*timeofday['SunriseInMinutes']
|
|
local goodnight = 60*timeofday['SunsetInMinutes']
|
|
local periode = goodnight - wakeup
|
|
local offset = wakeup-periode/2
|
|
local color = mini
|
|
local time = os.date("*t")
|
|
local now = 60*(time.hour*60 + time.min)
|
|
if now >= wakeup and now < goodnight then
|
|
color = math.floor((maxi-delta/2)+(delta/2)*math.cos((now-offset)*2*math.pi/periode)+0.5)
|
|
end
|
|
return color
|
|
end
|
|
|
|
-- régulation chauffage (PID)
|
|
--[[
|
|
|
|
usage:
|
|
|
|
local pid={}
|
|
pid['debug'] = true -- true pour voir les logs dans la console log Dz ou false pour ne pas les voir
|
|
pid['zone'] = 'salon' -- nom de la zone pour affichage dans les logs et ditinction de variables
|
|
pid['sonde'] = 'salon' -- Nom de la sonde de température
|
|
pid['OnOff'] = 'chauffage' -- Nom de l'interrupteur virtuel de mise en route (hivers/été)
|
|
pid['thermostat'] = 'th_salon' -- consigne ou 'nom' de l'interrupteur virtuel de thermostat
|
|
-- actionneur
|
|
pid['radiateur'] = 'radiateur salon' -- Nom de l'interrupteur de chauffage
|
|
pid['invert'] = false -- si On et Off doivent être inversé ou non
|
|
|
|
-- PID --
|
|
pid['Kp'] = 70 -- Coefficient proportionnel
|
|
pid['Ki'] = 8 -- Coefficient intégrateur
|
|
pid['Kd'] = 3 -- Coefficient dérivateur
|
|
|
|
pid['cycle'] = 15 -- temps en minute d'un cycle PID
|
|
pid['secu'] = 60 -- temps mini en seconde entre 2 ordres opposés
|
|
|
|
commandArray = {}
|
|
compute(pid)
|
|
return commandArray
|
|
|
|
]]
|
|
function compute(pid)
|
|
local init = 0
|
|
|
|
-- récupération température
|
|
local temp = getTemp(pid['sonde'])
|
|
-- récupération température ext
|
|
local temp_ext = nil
|
|
if pid['sonde_ext'] ~= '' and pid['sonde_ext'] ~= nil then
|
|
temp_ext = getTemp(pid['sonde_ext'])
|
|
end
|
|
|
|
-- création variable : 4 dernières températures
|
|
if (uservariables['PID_temps_'..pid['zone']] == nil ) then
|
|
creaVar('PID_temps_'..pid['zone'],string.rep(temp..';',3)..temp)
|
|
init = 1
|
|
end
|
|
-- création variable : intégrale
|
|
if (uservariables['PID_integrale_'..pid['zone']] == nil ) then
|
|
creaVar('PID_integrale_'..pid['zone'],'0')
|
|
init = 1
|
|
end
|
|
|
|
if init == 1 then
|
|
log('PID '..pid['zone']..' initialisation..',pid['debug'])
|
|
do return end
|
|
end
|
|
|
|
-- définition des variables locales
|
|
local moy_erreur = 0
|
|
local n = 1
|
|
local somme_erreurs = 0
|
|
local heatTime
|
|
local marche
|
|
local arret
|
|
local tmp = {}
|
|
|
|
-- définition des commandes marche/arrêt
|
|
if pid['invert'] then
|
|
marche = 'Off' ; arret = 'On'
|
|
else
|
|
marche = 'On' ; arret = 'Off'
|
|
end
|
|
|
|
-- à chaque cycle
|
|
if ( time.min%pid['cycle'] == 0 ) then
|
|
|
|
-- maj des 4 dernières temps
|
|
local temps = string.match(uservariables['PID_temps_'..pid['zone']],";([^%s]+)")..";"..temp
|
|
commandArray[#commandArray+1] = {['Variable:PID_temps_'..pid['zone']] = temps}
|
|
|
|
-- si l'on veut chauffer
|
|
if ( otherdevices[pid['OnOff']] == 'On' ) then
|
|
|
|
-- récupération de la consigne
|
|
local consigne = tonumber(otherdevices_svalues[pid['thermostat']]) or pid['thermostat']
|
|
-- calcul de l'erreur
|
|
local erreur = consigne-temp
|
|
-- calcul intégrale auto consumée et moyenne erreur glissante
|
|
temps:gsub("([+-]?%d+%.*%d*)",function(t)
|
|
tmp[n] = tonumber(t)
|
|
err = tonumber(consigne-t)
|
|
somme_erreurs = somme_erreurs+err
|
|
moy_erreur = moy_erreur+err*n^3
|
|
n = n+1
|
|
end)
|
|
|
|
somme_erreurs = round(constrain(somme_erreurs,0,255),1)
|
|
moy_erreur = round(moy_erreur/100,2)
|
|
|
|
-- calcul de la dérivée (régression linéaire - méthode des moindres carrés)
|
|
local delta_erreurs = round((4*(4*tmp[1]+3*tmp[2]+2*tmp[3]+tmp[4])-10*(tmp[1]+tmp[2]+tmp[3]+tmp[4]))/20,2)
|
|
|
|
-- aux abords de la consigne, passage au second systême integrale
|
|
if math.abs(somme_erreurs) < 2 then
|
|
somme_erreurs = tonumber(uservariables['PID_integrale_'..pid['zone']])
|
|
-- re calcule intégrale si hors hysteresis
|
|
-- à moins d'un dixièmes de degré d'écart avec la consigne
|
|
-- le ratrapage est considéré OK, l'intégrale n'est pas recalculée
|
|
if math.abs(erreur) > 0.11 then
|
|
-- calcule intégrale
|
|
somme_erreurs = round(constrain(somme_erreurs+erreur/2,0,5),2)
|
|
-- maj
|
|
commandArray[#commandArray+1] = {['Variable:PID_integrale_'..pid['zone']] = tostring(somme_erreurs)}
|
|
end
|
|
end
|
|
|
|
-- boucle ouverte,
|
|
-- modification dynamique des paramètres de régulation suivant température extérieure
|
|
local Kb = 0
|
|
if temp_ext ~= nil then
|
|
Kb = pid['Kb'] * (consigne - temp_ext - pid['ref']) / 100
|
|
end
|
|
pid['Kp'] = round(pid['Kp'] + pid['Kp'] * Kb)
|
|
pid['Ki'] = round(pid['Ki'] + pid['Ki'] * Kb)
|
|
pid['Kd'] = round(pid['Kd'] + pid['Kd'] * Kb)
|
|
|
|
-- calcul pid
|
|
local P = round(pid['Kp']*moy_erreur,2)
|
|
local I = round(pid['Ki']*somme_erreurs,2)
|
|
local D = round(pid['Kd']*delta_erreurs,2)
|
|
|
|
-- calcul de la commande en %
|
|
local commande = round(constrain(P+I+D,0,100))
|
|
|
|
-- calcul du temps de fonctionnement
|
|
if commande == 100 then
|
|
-- débordement de 20s pour ne pas couper avant recalcule
|
|
heatTime = (pid['cycle']*60)+20
|
|
elseif commande > 0 then
|
|
-- secu mini maxi
|
|
heatTime = round(constrain(commande*pid['cycle']*0.6,pid['secu'],(pid['cycle']*60)-pid['secu']))
|
|
elseif commande == 0 then
|
|
-- coupure retardée
|
|
heatTime = constrain(pid['secu']-lastSeen(pid['radiateur']),0,pid['secu'])
|
|
end
|
|
|
|
-- AFTER n'aime pas 1 ou 2..
|
|
if heatTime == 1 or heatTime == 2 then
|
|
heatTime = 0
|
|
end
|
|
|
|
-- action sur l'élément chauffant
|
|
if heatTime > 0 then
|
|
commandArray[#commandArray+1] = {[pid['radiateur']] = marche}
|
|
commandArray[#commandArray+1] = {[pid['radiateur']] = arret..' AFTER '..heatTime}
|
|
else
|
|
commandArray[#commandArray+1] = {[pid['radiateur']]=arret}
|
|
end
|
|
|
|
-- journalisation
|
|
if pid['debug'] then
|
|
log('PID zone: '..string.upper(pid['zone']))
|
|
if temp_ext ~= nil then
|
|
log('temperature ext: '..temp_ext..'°C')
|
|
end
|
|
log('température int: '..temp..'°C pour '..consigne..'°C souhaité')
|
|
log('Kp: '..pid['Kp'])
|
|
log('Ki: '..pid['Ki'])
|
|
log('Kd: '..pid['Kd'])
|
|
log('erreur: '..moy_erreur)
|
|
log('somme erreurs: '..somme_erreurs)
|
|
log('delta erreurs: '..delta_erreurs)
|
|
log('P: '..P)
|
|
log('I: '..I)
|
|
log('D: '..D)
|
|
log('cycle: '..pid['cycle']..'min (sécu: '..pid['secu']..'s)')
|
|
-- avertissement si secu dépasse 1/4 du cycle
|
|
if ((100*pid['secu'])/(60*pid['cycle'])>25) then
|
|
warn('sécu trop importante, ralonger durée de cycle..')
|
|
end
|
|
log('commande: '..commande..'% ('..string.sub(os.date("!%X",heatTime),4,8):gsub("%:", "\'")..'\")')
|
|
log('')
|
|
end
|
|
|
|
-- maj sonde virtuelle
|
|
--commandArray[#commandArray+1] = {['UpdateDevice'] = otherdevices_idx[pid['sonde']..'_pid']..'|0|'..temp..';'..commande..';0'}
|
|
|
|
end
|
|
|
|
end
|
|
-- toutes les 15 minutes, si on ne veut pas chauffer
|
|
if ( time.min%15 == 0 and otherdevices[pid['OnOff']] == 'Off' ) then
|
|
|
|
-- arrêt chauffage (renvoi commande systematique par sécurité)
|
|
commandArray[#commandArray+1] = {[pid['radiateur']] = arret..' AFTER '..constrain(pid['secu']-lastSeen(pid['radiateur']),3,pid['secu'])}
|
|
|
|
-- maj sonde virtuelle
|
|
--commandArray[#commandArray+1] = {['UpdateDevice'] = otherdevices_idx[pid['sonde']..'_pid']..'|0|'..temp..';0;0'}
|
|
end
|
|
|
|
end
|
|
|
|
-- détermination automatique des paramètres de régulation PID
|
|
function autotune(pid)
|
|
|
|
-- http://brettbeauregard.com/blog/2012/01/arduino-pid-autotune-library/
|
|
|
|
if devicechanged[pid['sonde']] then
|
|
|
|
-- définition des commandes marche/arrêt
|
|
if pid['invert'] then
|
|
marche = 'Off' ; arret = 'On'
|
|
else
|
|
marche = 'On' ; arret = 'Off'
|
|
end
|
|
|
|
-- récupération température
|
|
local temp = getTemp(pid['sonde'])
|
|
-- récupération consigne
|
|
local consigne = tonumber(otherdevices_svalues[pid['thermostat']]) or pid['thermostat']
|
|
|
|
-- hysteresis
|
|
if temp > consigne then
|
|
commandArray[#commandArray+1] = {[pid['radiateur']] = arret}
|
|
elseif temp < consigne then
|
|
commandArray[#commandArray+1] = {[pid['radiateur']] = marche}
|
|
end
|
|
|
|
-- timestamp
|
|
local now = os.time()
|
|
|
|
-- save all temps
|
|
logToFile(pid['zone']..'_temps',now..';'..temp)
|
|
|
|
local max1 = consigne
|
|
local max2 = consigne
|
|
local mini = consigne
|
|
local max1_ts = now
|
|
local max2_ts = now
|
|
local mini_ts = now
|
|
local init = 0
|
|
|
|
-- recherche des valeurs mini/maxi
|
|
for _,v in spairs(ReverseTable(lines_from(pid['zone']..'_temps'))) do
|
|
t = tonumber(string.match(v,";([^%s]+)"))
|
|
ts = tonumber(string.match(v,"([^%s]+);"))
|
|
if t < mini and init == 0 then
|
|
init = 1
|
|
elseif t > max2 and (init == 1 or init == 2) then
|
|
init = 2
|
|
max2 = t
|
|
max2_ts = ts
|
|
elseif t < mini and (init == 2 or init == 3) then
|
|
init = 3
|
|
mini = t
|
|
mini_ts = ts
|
|
elseif t > max1 and (init == 3 or init == 4) then
|
|
init = 4
|
|
max1 = t
|
|
max1_ts = ts
|
|
elseif t <= consigne and init == 4 then
|
|
init = 5
|
|
break
|
|
end
|
|
end
|
|
|
|
-- autotune
|
|
local Pu = round(os.difftime(max2_ts, max1_ts) / 60)
|
|
local A = tonumber(max1 - mini)
|
|
local Ku = round(200 / (A * math.pi))
|
|
local Kp = round(0.6 * Ku)
|
|
local Ki = round(1.2 * Ku / Pu * pid['cycle'])
|
|
local Kd = round(0.075 * Ku * Pu / pid['cycle'])
|
|
|
|
-- journalisation
|
|
log('PID autotune '..string.upper(pid['zone']))
|
|
if init == 5 then
|
|
log('max1: '..max1..' a '..os.date('%H:%M', max1_ts),pid['debug'])
|
|
log('mini: '..mini..' a '..os.date('%H:%M', mini_ts),pid['debug'])
|
|
log('max2: '..max2..' a '..os.date('%H:%M', max2_ts),pid['debug'])
|
|
log('Pu:'..Pu,pid['debug'])
|
|
log('A:'..A,pid['debug'])
|
|
log('Ku:'..Ku,pid['debug'])
|
|
log('Kp:'..Kp..' Ki:'..Ki..' Kd:'..Kd)
|
|
else
|
|
log('mesures en cours..')
|
|
end
|
|
|
|
end
|
|
end
|