-- Module L_SmartMeter.lua
-- Written by R.Boer.
-- V1.0 5 March 2015
--
-- Read from Smater Meters
-- See forum topic : http://f...content-available-to-author-only...e.com/index.php/topic,10736.0.html
-- and for this plug in :
local Version = "1.0 rboer"
local DESCRIPTION = "Smart Meter"
local SM_SID = "urn:rboer-com:serviceId:SmartMeter1"
local EM_SID = "urn:micasaverde-com:serviceId:EnergyMetering1"
local GE_SID = "urn:micasaverde-com:serviceId:GenericSensor1"
local PM_XML = "D_PowerMeter1.xml"
local GE_XML = "D_GenericSensor1.xml"
local THIS_DEVICE = 0
local syslog
local LogLevel = 3
local ShowMultiTariff = 0
local ShowExport = 0
local ShowGas = 0
local ShowWholeHouse = 0
local UseGeneratedPower = 0
local indGasComming = false -- Some meter types split Gas reading over two lines
local powerGeneratorCount = 0
local powerGeneratorDevice = {}
local Icon = {
Variable = "IconSet", -- Variable controlling the iconsVariable
IDLE = '0', -- No background
OK = '1', -- Green
BUSY = '2', -- Blue
WAIT = '3', -- Amber
ERROR = '4' -- Red
}
local TaskData = {
Description = "Smart Meter",
taskHandle = -1,
ERROR = 2,
ERROR_PERM = -2,
SUCCESS = 4,
BUSY = 1
}
---------------------------------------------------------------------------------------------
-- Utility functions
---------------------------------------------------------------------------------------------
function log(text, level)
local level = (level or 10)
if (LogLevel >= level) then
if (syslog) then
local slvl
if (level == 1) then slvl = 2
elseif (level == 2) then slvl = 4
elseif (level == 3) then slvl = 5
elseif (level == 4) then slvl = 5
elseif (level == 7) then slvl = 6
elseif (level == 8) then slvl = 6
else slvl = 7
end
syslog:send(text,slvl)
else
if (level == 10) then level = 50 end
print(DESCRIPTION .. ": " .. text or "no text")
end
end
end
-- Get variable value.
-- Use SM_SID and THIS_DEVICE as defaults
local function varGet(name, device, service)
-- local value = luup.variable_get(service or SM_SID, name, tonumber(device or THIS_DEVICE))
return (value or '')
end
-- Update variable when value is different than current.
-- Use SM_SID and THIS_DEVICE as defaults
local function varSet(name, value, device, service)
local service = service or SM_SID
local device = tonumber(device or THIS_DEVICE)
local old = varGet(name, device, service)
if (tostring(value) ~= old) then
-- luup.variable_set(service, name, value, device)
end
end
--get device Variables, creating with default value if non-existent
local function defVar(name, default, device, service)
local service = service or SM_SID
local device = tonumber(device or THIS_DEVICE)
local value = luup.variable_get(service, name, device)
if (not value) then
value = default or '' -- use default value or blank
luup.variable_set(service, name, value, device) -- create missing variable with default value
end
return value
end
-- Set message in task window.
local function task(text, mode)
local mode = mode or TaskData.ERROR
if (mode ~= TaskData.SUCCESS) then
if (mode == TaskData.ERROR_PERM) then
log("task: " .. (text or "no text"), 1)
else
log("task: " .. (text or "no text"))
end
end
TaskData.taskHandle = luup.task(text, (mode == TaskData.ERROR_PERM) and TaskData.ERROR or mode, TaskData.Description, TaskData.taskHandle)
end
-- Set a luup failure message
local function setluupfailure(status,devID)
if (luup.version_major < 7) then status = status ~= 0 end -- fix UI5 status type
luup.set_failure(status,devID)
end
-- Syslog server support. From Netatmo plugin by akbooer
function syslog_server (ip_and_port, tag, hostname)
local sock = socketLib.udp()
local facility = 1 -- 'user'
local emergency, alert, critical, error, warning, notice, info, debug = 0,1,2,3,4,5,6,7
local ip, port = ip_and_port: match "^(%d+%.%d+%.%d+%.%d+):(%d+)$"
if not ip or not port then return nil, "invalid IP or PORT" end
local serialNo = luup.pk_accesspoint
hostname = ("Vera-"..serialNo) or "Vera"
if not tag or tag == '' then tag = DESCRIPTION end
tag = tag:gsub("[^%w]","") or DESCRIPTION -- only alphanumeric, no spaces or other
local function send (self, content, severity)
content = tostring (content)
severity = tonumber (severity) or info
local priority = facility*8 + (severity%8)
local msg = ("<%d>%s %s %s: %s\n"):format (priority, os.date "%b %d %H:%M:%S", hostname, tag, content)
sock:send (msg)
end
local ok, err = sock:setpeername(ip, port)
if ok then ok = {send = send} end
return ok, err
end
-- Set the status Icon
function setStatusIcon(status)
if (status == Icon.OK) then
-- When status is success, then clear after number of seconds
local idleDelay = varGet("OkInterval")
if (tonumber(idleDelay) > 0) then
varSet(Icon.Variable, Icon.OK)
luup.call_delay("idleStatusIcon", idleDelay, "", false)
else
varSet(Icon.Variable, Icon.IDLE)
end
else
varSet(Icon.Variable, status)
end
end
function idleStatusIcon()
local status = varGet(Icon.Variable)
-- When status is success, then clear else do not change
if (status == Icon.OK) then
varSet(Icon.Variable, Icon.IDLE)
end
end
-- Luup Reload function for UI5,6 and 7
local function luup_reload()
if (luup.version_major < 6) then
luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "Reload", {}, 0)
else
luup.reload()
end
end
-- Keys for Smart Meter values
local Mt = "/"
local ImpT1 = "1-0:1.8.1"
local ImpT2 = "1-0:1.8.2"
local ExpT1 = "1-0:2.8.1"
local ExpT2 = "1-0:2.8.2"
local ImpWatts = "1-0:1.7.0"
local ExpWatts = "1-0:2.7.0"
local Ta = "0-0:96.14.0"
local Gas2 = "0-1:24.3.0"
local Gas = "0-1:24.2.1"
local WholeHouse = "9-9:9.9.9" -- Dummy Meter reading type
-- Functions to parse meter values from strings
local function SetImpKWH(devID, Key, dataStr)
-- if (devID == nil) then return nil end
local newVal = tonumber(dataStr:match("%d+.%d+"))
local elem = mapperData[Key]
if (newVal ~= elem.val) then
elem.val = newVal
if (ShowMultiTariff == 0) then
-- We are not showing multiple tariffs, so add T1 and T2 KWH for user when both have been read
if (mapperData[ImpT1].val ~= -1) and (mapperData[ImpT2].val ~= -1) then
varSet(elem.var, mapperData[ImpT1].val + mapperData[ImpT2].val, devID, EM_SID)
end
else
varSet(elem.var, newVal, devID, EM_SID)
end
end
return newVal
end
local function SetExpKWH(devID, Key, dataStr)
-- if (devID == nil) then return nil end
local newVal = tonumber(dataStr:match("%d+.%d+"))
local elem = mapperData[Key]
if (newVal ~= elem.val) then
elem.val = newVal
if (ShowMultiTariff == 0) then
-- We are not showing multiple tariffs, so add T1 and T2 KWH for user when both have been read
if (mapperData[ExpT1].val ~= -1) and (mapperData[ExpT2].val ~= -1) then
varSet(elem.var, mapperData[ExpT1].val + mapperData[ExpT2].val, devID, EM_SID)
end
else
varSet(elem.var, newVal, devID, EM_SID)
end
end
return newVal
end
local function SetWatts(devID, Key, dataStr)
-- if (devID == nil) then return nil end
local newVal = tonumber(dataStr:match("%d+.%d+"))*1000
local elem = mapperData[Key]
if (newVal ~= elem.val) then
varSet(elem.var, newVal, devID, EM_SID)
elem.val = newVal
end
return newVal
end
local function SetGas(devID, Key, dataStr)
-- if (devID == nil) then return nil end
local newVal = tonumber(string.match(dataStr:match("(%d+.%d+*m3)"), "%d+.%d+"))
local elem = mapperData[Key]
if (newVal ~= elem.val) then
varSet(elem.var, newVal, devID, GE_SID)
elem.val = newVal
end
return newVal
end
local function SetTarif(devID, Key, dataStr)
local newVal = tonumber(dataStr:match("%d+"))
local elem = mapperData[Key]
if (newVal ~= elem.val) then
elem.val = newVal
if (newVal == 1) then
mapperData[ImpWatts].dev = mapperData[ImpT1].dev
mapperData[ExpWatts].dev = mapperData[ExpT1].dev
varSet(elem.var, 1)
elseif (newVal == 2) then
mapperData[ImpWatts].dev = mapperData[ImpT2].dev
mapperData[ExpWatts].dev = mapperData[ExpT2].dev
varSet(elem.var, 2)
end
end
return newVal
end
local function SetMeter(devID, Key, dataStr)
local elem = mapperData[Key]
if (dataStr ~= elem.val) then
varSet(elem.var, dataStr)
elem.val = dataStr
end
return dataStr
end
local function SetGas2(devID, Key, dataStr)
-- if (mapperData[Gas].dev == nil) then return false end
log ("SetGas2 "..Key.." data "..dataStr)
indGasComming = true
log ("SetGas2 end")
return ""
end
-- Mapping for each key value we are interested in.
mapperData = {
[Mt] = {var = "MeterType", dev = nil, val = "", func = SetMeter, ChildDesc = "", xml = nil},
[Ta] = {var = "ActiveTariff", dev = nil, val = 0, func = SetTarif, ChildDesc = "", xml = nil},
[ImpT1] = {var = "KWH", dev = nil, val = -1, func = SetImpKWH, ChildDesc = "ImportT1", xml = PM_XML},
[ImpT2] = {var = "KWH", dev = nil, val = -1, func = SetImpKWH, ChildDesc = "ImportT2", xml = PM_XML},
[ExpT1] = {var = "KWH", dev = nil, val = -1, func = SetExpKWH, ChildDesc = "ExportT1", xml = PM_XML},
[ExpT2] = {var = "KWH", dev = nil, val = -1, func = SetExpKWH, ChildDesc = "ExportT2", xml = PM_XML},
[ImpWatts] = {var = "Watts", dev = nil, val = 0, func = SetWatts, ChildDesc = "", xml = nil},
[ExpWatts] = {var = "Watts", dev = nil, val = 0, func = SetWatts, ChildDesc = "", xml = nil},
[Gas2]= {var = "N/A", dev = nil, val = 0, func = SetGas2, ChildDesc = "", xml = nil},
[Gas]= {var = "CurrentLevel", dev = nil, val = 0, func = SetGas, ChildDesc = "ExportGas", xml = GE_XML},
[WholeHouse]= {var = "Watts", dev = nil, val = 0, func = nil, ChildDesc = "WholeHouse", xml = PM_XML}
}
-- Find the device ID of the type
function findChild(label)
for k, v in pairs(luup.devices) do
if (v.device_num_parent == THIS_DEVICE and v.id == "SM_"..mapperData[label].ChildDesc) then
mapperData[label].dev = k
return true
end
end
-- Dump a copy of the Global Module list for debugging purposes.
for k, v in pairs(luup.devices) do
log("Device Number: " .. k ..
" v.device_type: " .. tostring(v.device_type) ..
" v.device_num_parent: " .. tostring(v.device_num_parent) ..
" v.id: " .. tostring(v.id))
end
return false
end
-- Start up plug in
function SmartMeter_Init(lul_device)
THIS_DEVICE = lul_device
log("Starting SmartMeter device: " .. tostring(THIS_DEVICE),3)
ShowMultiTariff = defVar("ShowMultiTariff",0) -- When 1 show T1 and T2 separately
ShowExport = defVar("ShowExport",0) -- When 1 show Import and Export separately
ShowWholeHouse = defVar("ShowWholeHouse",0) -- When 1 show separate WholeHouse calculation
UseGeneratedPower = defVar("UseGeneratedPower",0) -- When 1 use power generated (e.g. solar) in calculations
local GeneratedPowerSources = defVar("GeneratedPowerSources") -- Device ID list of power generating sources (e.g. solar) in calculations
ShowGas = defVar("ShowGas",0) -- When 1 show Import and Export separately
defVar("Gas",0)
mapperData[Ta].val = defVar("ActiveTariff",1)
defVar("MeterType", "Unknown")
-- Create the child devices the user wants
local childDevices = luup.chdev.start(THIS_DEVICE);
-- Create devices needed if not exist
addMeterDevice(childDevices,ImpT1)
if (ShowMultiTariff == 1) then addMeterDevice(childDevices,ImpT2) end
if (ShowExport == 1) then
addPowerMeterDevice(childDevices,ExpT1)
if (ShowMultiTariff == 1) then addMeterDevice(childDevices,ExpT2) end
end
if (ShowWholeHouse == 1) then addMeterDevice(childDevices,WholeHouse) end
if (ShowGas == 1) then addMeterDevice(childDevices,Gas) end
-- Vera will reload here when there are new devices or changes to a child
luup.chdev.sync(HData.DEVICE, childDevices)
-- Pickup device IDs from names
findChild(ImpT1)
if (ShowMultiTariff == 1) then
findChild(ImpT2)
else
mapperData[ImpT2].dev = mapperData[ImpT1].dev
end
if (ShowExport == 1) then
findChild(ExpT1)
if (ShowMultiTariff == 1) then
findChild(ExpT2)
else
mapperData[ExpT2].dev = mapperData[ExpT1].dev
end
end
if (ShowWholeHouse == 1) then findChild(WholeHouse) end
if (ShowGas == 1) then findChild(Gas) end
-- For current watt readings map to right device ID
if (ShowMultiTariff == 1) then
if (mapperData[Ta].val == 1) then
mapperData[ImpWatts].dev = mapperData[ImpT1].dev
if (ShowExport == 1) then mapperData[ExpWatts].dev = mapperData[ExpT1].dev end
else
mapperData[ImpWatts].dev = mapperData[ImpT2].dev
if (ShowExport == 1) then mapperData[ExpWatts].dev = mapperData[ExpT2].dev end
end
else
mapperData[ImpWatts].dev = mapperData[ImpT1].dev
if (ShowExport == 1) then mapperData[ExpWatts].dev = mapperData[ExpT1].dev end
end
log("SmartMeter has started...")
setluupfailure(0, THIS_DEVICE)
return true
end
-- Find child based on having THIS_DEVICE as parent and the expected altID
function addMeterDevice(childDevices,meterID)
local elem = mapperData[meterID]
local meterName = "SM_"..elem.ChildDesc
local childName = "SmartMeter "..elem.ChildDesc
-- Now add the new device to the tree
log("Creating child device id " .. meterName .. " (" .. childName .. ")")
luup.chdev.append(
THIS_DEVICE, -- parent (this device)
childDevices, -- pointer from above "start" call
meterName, -- child Alt ID
childName, -- child device description
"", -- serviceId (keep blank for UI7 restart avoidance)
elem.xml, -- device file for given device
"", -- Implementation file
"", -- parameters to set
true) -- not embedded child devices can go in any room
end
---------------------------------------------------------------------------------------------
-- Data line has been received via serial. Process
---------------------------------------------------------------------------------------------
function SmartMeter_Incoming(data)
if (data:len() > 0) then
if (indGasComming == true) then
log("indGasComming is true")
-- GAS on ISK5 where GAS reading is on its own line.
local newVal = tonumber(string.match(data, "%d+.%d+"))
log("Gas meter: [" .. newVal .. "]")
local elem = mapperData[Gas]
if (newVal ~= elem.val) then
varSet("CurrentLevel", newVal, elem.dev, GE_SID)
elem.val = newVal
end
indGasComming = false
else
-- Get line key
local Key = data:match("[0-9%:%-%.%/]+")
if Key then
local elem = mapperData[Key]
if elem then
local res, val = pcall(mapperData[Key].func,elem.dev,Key,data:sub(Key:len()+1))
if res then
log("Found key : "..Key.." for "..elem.var.." to set to value "..val)
else
log("Found key : "..Key.." for "..elem.var.." but failed to obtain value ",7)
end
else
log("Not processing : "..data)
end
else
log("No key found in : "..data)
end
end
end
end
function SmartMeter_Test()
LogLevel=10
indGasComming=false
-- Meter types
SmartMeter_Incoming("/ISk5\2MT382-1003")
SmartMeter_Incoming("/KFM5KAIFA-METER")
-- ActiveTariff
SmartMeter_Incoming("0-0:96.14.0(0001)")
-- InpT1, InpT2, ExpT1, ExpT2
SmartMeter_Incoming("1-0:1.8.1(000231.094*kWh)")
SmartMeter_Incoming("1-0:1.8.2(000277.981*kWh)")
SmartMeter_Incoming("1-0:2.8.1(000066.164*kWh)")
SmartMeter_Incoming("1-0:2.8.2(000174.446*kWh)")
-- ImpWatts, ExpWatts
SmartMeter_Incoming("1-0:1.7.0(02.104*kW)")
SmartMeter_Incoming("1-0:2.7.0(01.010*kW)")
-- Gas for Kaifa
SmartMeter_Incoming("0-1:24.2.1(140828150000S)(00048.320*m3)")
-- Gas for ISk5
SmartMeter_Incoming("0-1:24.3.0(150301230000)(00)(60)(1)(0-1:24.2.1)(m3)")
SmartMeter_Incoming("(02351.200)" )
SmartMeter_Incoming("0-1:24.4.0(1)" )
SmartMeter_Incoming("0-0:96.1.1(5A424556303035313136323433303132)")
SmartMeter_Incoming("0-1:24.1.0(3)")
SmartMeter_Incoming("0-0:96.13.0()")
SmartMeter_Incoming("0-0:96.3.10(1)")
SmartMeter_Incoming("0-0:17.0.0(0999.00*kW)")
SmartMeter_Incoming("0-1:96.1.0(3238303131303031323439303539333132)")
SmartMeter_Incoming("1-0:31.7.0(000*A)")
SmartMeter_Incoming("1-0:41.7.0(00.080*kW)")
SmartMeter_Incoming("!A89C")
SmartMeter_Incoming("!")
end
SmartMeter_Test()