1054 lines
33 KiB
Lua
Executable File
1054 lines
33 KiB
Lua
Executable File
--[[
|
||
|
||
|
||
|
||
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.1.3' --'127.0.0.1'
|
||
domoticzPORT = '81'
|
||
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 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
|
||
|
||
function humiditeAbsolue(T, rh)
|
||
-- https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
|
||
|
||
-- Formula for calculating absolute humidity
|
||
-- In the formula below, temperature (T) is expressed in degrees Celsius, relative humidity (rh) is expressed in %, and e is the base of natural logarithms 2.71828 [raised to the power of the contents of the square brackets]:
|
||
-- Absolute Humidity (grams/m3) = 6.112 × e^[(17.67 × T)/(T+243.5)] × rh × 18.02
|
||
--(273.15+T) × 100 × 0.08314
|
||
-- which simplifies to
|
||
-- Absolute Humidity (grams/m3) = 6.112 × e^[(17.67 × T)/(T+243.5)] × rh × 2.1674 / (273.15+T)
|
||
-- This formula is accurate to within 0.1% over the temperature range –30°C to +35°C
|
||
|
||
local absoluteH = 6.112 * math.pow(2.71828, (17.67 * T) / (T + 243.5)) * rh * 2.1674 / (273.15+T)
|
||
|
||
print("Humidité absolute T="..tostring(T)..' H='..tostring(rh)..' '..tostring(absoluteH))
|
||
|
||
return absoluteH
|
||
end
|