Code: Select all
--[[
Purpose of the script is to get data from a Philips purifier and update the corresponding domoticz devices
Prerequisite is an installed and working air-control.
This can be found here https://github.com/rgerganov/py-air-control
Thx to @martijnvdijk for initial idea, testing and feedback
history:
20201006 waaren Start coding
20201007 waaren Add extra loglines and some devices
20201007 waaren Sort filtertimes for alertDevice
20201007 waaren Add notification/email to timely order and/or clean filter(s)
20201010 waaren Executing airctrl in background for increased resilience
20201012 waaren Add extra debug logging
20201014 martijnvdijk Intercept and convert to number when fanspeed value is reported as string
20201014 waaren Add cumulatedRuntime function
20201015 waaren Public release on domoticz forum
key / values available:
-----------------------
Active carbon filter type: String
WifiVersion: string
DeviceId: string
Active carbon filter: string with hours until replacement as integer
Child lock: Boolean
Runtime: string with hours as float
StatusType: string
Version: string
Water level: integer
Target humidity: integer
Power: string (ON/OFF)
ModelId: string
Function: string
Wick filter: string with hours until replacement as integer
Temperature: integer
Light brightness: integer
Allergen index: integer range
Used index: string
Air quality notification threshold: integer range
Type: string
rddp: integer
HEPA filter: string with hours until replacement as integer
PM25: integer range
Mode: string
Humidity: integer
Name: string
ProductId: string
Over the air updates: string
HEPA filter type: string
Pre-filter and Wick: string with hours until cleaning as integer
ConnectType: string
Fan speed: integer range
Buttons light: string ON/OFF
implemented
-----------
Fan speed: Custom sensor
Power: Switch
Humidity: tempHum
Temperature: tempHum
PM25: Custom sensor
Allergen index: Custom sensor
rddp:0 Text Sensor
Water level: Percentage
Pre-filter and Wick: Custom Sensor + Alert + Notification + Email
HEPA filter: Custom Sensor + Alert + Notification + Email
Active carbon filter: Custom Sensor + Alert + Notification + Email
Wick filter: Custom Sensor + Alert + Notification + Email
Runtime: Custom Sensor
-]]
scriptVar = 'Purifier'
return
{
on =
{
timer =
{
'every 5 minutes', -- Once per 5 minutes. Change to the frequency you need
},
customEvents =
{
scriptVar .. '*',
},
},
data =
{
repeats =
{
initial = 0,
},
lastNotification =
{
initial = 0,
},
lastRuntime =
{
initial = 0,
},
cumulatedRuntime =
{
initial = 0,
},
},
logging =
{
level = domoticz.LOG_DEBUG, -- change to domoticz.LOG_ERROR when all ok.
marker = scriptVar,
},
execute = function(dz, item)
------------------------------------------------------------------- Your settings below this line
-- devices
local tempHum = dz.devices('Purifier temperature') -- define as virtual temperature / Humidity sensor
local status = dz.devices('Purifier status') -- define as virtual text sensor
local alert = dz.devices('Purifier alert') -- define as virtual alert device
local power = dz.devices('Purifier power') -- define as virtual switch
local water = dz.devices('Purifier waterlevel') -- define as percentage
local airQuality = dz.devices('Purifier airquality') -- define as custom sensor (use µg/m3 as X-level)
local allergen = dz.devices('Purifier allergen index') -- define as custom sensor (use space as X-level)
local runtime = dz.devices('Purifier runtime') -- define as custom sensor (hours)
local fanspeed = dz.devices('Purifier fanspeed') -- define as custom sensor (use space as X-level)
local HEPAFilter = dz.devices('Purifier HEPA filter') -- define as custom sensor (hours)
local carbonFilter = dz.devices('Purifier carbon filter') -- define as custom sensor (hours)
local wickFilter = dz.devices('Purifier wick filter') -- define as custom sensor (hours)
local preFilter = dz.devices('Purifier pre filter') -- define as custom sensor (hours)
-- (temp) files
local purifyData = '/tmp/purifyData'
-- IP address
local ipAddress = 'xxx.xxx.xxx.xxx'
local myNotificationTable =
{
-- table with one or more notification systems.
-- uncomment the notification systems that you want to be used
-- Can be one or more of
-- dz.NSS_FIREBASE_CLOUD_MESSAGING,
dz.NSS_PUSHOVER,
-- dz.NSS_HTTP,
-- dz.NSS_KODI,
-- dz.NSS_LOGITECH_MEDIASERVER,
-- dz.NSS_NMA,
-- dz.NSS_PROWL,
-- dz.NSS_PUSHALOT,
-- dz.NSS_PUSHBULLET,
-- dz.NSS_PUSHOVER,
-- dz.NSS_PUSHSAFER,
dz.NSS_TELEGRAM,
}
-- various settings
local maxRepeats = 5 -- Try max this number of times before giving up
local notificationTimeWindow = '09:00-23:00' -- no messages outside this window
local notifyBeforeHour = 120 -- Hours before required filter cleaning / replacement to receive notification
local emailNotification = '[email protected]' -- remove this line if you do not want an Email notification
----
------------------------------------------------------------------- No changes required below this line
-- os Commands
local createPurifyDataCommand = 'sudo airctrl --ipaddr ' .. ipAddress .. ' --protocol coap > ' .. purifyData .. ' &' -- & ==>> force command to background
local getPurifyDataCommand = 'sudo cat ' .. purifyData
local killProcessCommand = 'sudo pkill -ec airctrl' -- kill process and display processes killed + count of killed processes
local cleanup = 'sudo rm ' .. purifyData
-- dz.emitStrings
local startOver = scriptVar .. '_startOver'
local collectData = scriptVar .. '_collectData'
-- functions
local function dumper(data, name)
if _G.logLevel == dz.LOG_DEBUG then
if type(data) ~= 'table' then
dz.log((name or 'string Dumper') .. ': ' .. dz.utils._.str(data):gsub('\n','; ') ,dz.LOG_DEBUG)
else
dz.log((name or 'table Dumper') .. ': ___________',dz.LOG_DEBUG)
dz.utils.dumpTable(data)
end
end
end
local function osCommand(cmd) -- osCommmand is also a native function ( dz.utils.osCommand ) in domoticz >= V2020.2 build 12394 (dzVents 3.0.13)
local file = io.popen(cmd)
local output = file:read('*all')
local rc = { file:close() }
dumper(output, 'from osCommand' )
return output, rc[3]
end
local function data2Table(str)
keyTable = {}
for line in str:gmatch('([^\n]*)\n?') do
local key = line:match('%b]:'):sub(3,-2):match('^%s*(.*)'):gsub('%W','_') -- %w is all letters and digits so %W is everything else
local value = line:match(':%s(.*)')
local value = tonumber(value) or value
keyTable[key] = value
if line:find('%d hours') and line:find('ilter') then
keyTable[key .. '_Counter'] = tonumber(keyTable[key]:match("%d+"))
end
if value == 'turbo' then keyTable[key] = 4 end
if value == 'silent' then keyTable[key] = 0.5 end
end
dumper(keyTable, 'from data2Table')
return keyTable
end
local function humidityStatus(temperature, humidity)
if humidity <= 30 then return dz.HUM_DRY
elseif humidity >= 70 then return dz.HUM_WET
elseif humidity >= 35 and
humidity <= 65 and
temperature >= 22 and
temperature <= 26 then return dz.HUM_COMFORTABLE
else return dz.HUM_NORMAL end
end
local function alertLevel(hours)
hours = tonumber(hours) or -1
if hours < ( notifyBeforeHour / 4 ) then return dz.ALERTLEVEL_RED end
if hours < ( notifyBeforeHour / 2 ) then return dz.ALERTLEVEL_YELLOW end
if hours < notifyBeforeHour then return dz.ALERTLEVEL_ORANGE end
return dz.ALERTLEVEL_GREEN
end
local function notification(filters)
if dz.time.matchesRule('at ' .. notificationTimeWindow) and dz.data.lastNotification < os.time() - 72000 then -- at least 20 hours after previous notification
dz.notify('Purifier', 'filter(s) need cleaning or replacement:\n' .. filters, dz.PRIORITY_NORMAL,dz.SOUND_DEFAULT, '' , myNotificationTable)
dz.data.lastNotification = os.time()
if emailNotification then
dz.email('Purifier', 'filter(s) need cleaning or replacement:\n' .. filters, emailNotification)
end
end
end
local function sortTable(t, order)
-- collect the keys
local keys = {}
for k in pairs(t) do keys[#keys+1] = k end
-- if order function given, sort by it by passing the table and keys a, b,
-- otherwise just sort the keys
if order then
table.sort(keys, function(a,b) return order(t, a, b) end)
else
table.sort(keys)
end
-- return the iterator function
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], t[keys[i] ]
end
end
end
local function combineAlerts(t)
local alertText = ''
local notificationText = ''
local filters = {}
local maxLevel = 0
-- select records and determine maxLevel
for key, hours in pairs(t) do
if key:find('_Counter') then
dz.log(key .. ': ' .. hours,dz.LOG_DEBUG)
maxLevel = math.max(maxLevel, alertLevel(hours))
filters[key] = tonumber(hours)
end
end
-- sort records and create String
for key, hours in sortTable(filters, function(t ,a ,b ) return t[b] > t[a] end) do
alertText = alertText .. key:gsub('_Counter',''):gsub('_',' ') .. ': ' .. hours .. '\n'
if hours < notifyBeforeHour then
notificationText = notificationText .. key:gsub('_Counter',''):gsub('_',' ') .. ' within ' .. hours .. ' hours\n'
end
end
return maxLevel, alertText, ( notificationText ~= '' and notificationText )
end
local function cumulatedRuntime(str)
local hours = tonumber(str:match("%.*(%d+%.*%d*)%.*")) -- isolate number from runtime string
if hours < dz.data.lastRuntime then -- hours reset to 0 ?!
dz.data.cumulatedRuntime = dz.data.cumulatedRuntime + dz.data.lastRuntime
end
dz.data.lastRuntime = hours
return ( hours + dz.data.cumulatedRuntime ) .. ' hours'
end
local function processReturn(t)
-- Text sensor
status.updateText('status: ' .. t.StatusType .. '\n' .. 'HEPA filter: ' .. t.HEPA_filter.. '\n' ..'rddp: ' .. t.rddp .. '\n' ..
'Carbon filter: ' .. t.Active_carbon_filter.. '\n' ..'Wick filter: ' .. t.Wick_filter)
-- Temperature / Humidity
tempHum.updateTempHum(t.Temperature, t.Humidity, humidityStatus(t.Temperature, t.Humidity))
--Alert sensor
local maxLevel, alertText, notificationText = combineAlerts(t)
alert.updateAlertSensor(maxLevel, alertText)
--Percentage
water.updatePercentage(t.Water_level)
-- On/Off switch
if t.Power == 'ON' then
power.switchOn().checkFirst()
else
power.switchOff().checkFirst()
end
--Custom sensors
airQuality.updateCustomSensor(t.PM25)
allergen.updateCustomSensor(t.Allergen_index)
fanspeed.updateCustomSensor(t.Fan_speed)
HEPAFilter.updateCustomSensor(t.HEPA_filter_Counter)
carbonFilter.updateCustomSensor(t.Active_carbon_filter_Counter)
wickFilter.updateCustomSensor(t.Wick_filter_Counter)
preFilter.updateCustomSensor(t.Pre_filter_and_Wick_Counter)
-- "managed" Custom sensors
runtime.updateCustomSensor(cumulatedRuntime(t.Runtime))
--Notification / email
if notificationText then
notification(notificationText)
end
end
-- main
if item.isDevice or item.isTimer or item.trigger == startOver then
osCommand(createPurifyDataCommand) -- (re)start the sequence
dz.emitEvent(collectData).afterSec(10)
elseif item.trigger == collectData then
if dz.utils.fileExists(purifyData) then
local purifyDataString, rc = osCommand(getPurifyDataCommand)
if rc ~= 0 or #purifyDataString < 500 then
dz.log('Unexpected problem retrieving data from Purifier. Giving up now', dz.LOG_ERROR)
dz.log('Content of purify datafile: ' .. purifyDataString .. '; ReturnCode is: ' .. rc , dz.LOG_DEBUG)
else
processReturn(data2Table(purifyDataString))
dz.data.repeats = 0
osCommand(cleanup)
end
else
osCommand(killProcessCommand) -- Process did not return data so assume it hangs
dz.data.repeats = dz.data.repeats + 1
if dz.data.repeats >= maxRepeats then
dz.notify('Purifier', 'Tried ' .. dz.data.repeats .. ' times. Giving up now.', dz.PRIORITY_HIGH,dz.SOUND_DEFAULT, '' , myNotificationTable)
else
dz.log('Unexpected problem retrieving data from Purifier. Retrying', dz.LOG_ERROR)
dz.emitEvent(startOver).afterSec(10 + 60 * dz.data.repeats) -- increasing delay
end
end
end
end
}