fork download
  1. -- Module L_SmartMeter.lua
  2. -- Written by R.Boer.
  3. -- V1.0 5 March 2015
  4. --
  5. -- Read from Smater Meters
  6. -- See forum topic : http://f...content-available-to-author-only...e.com/index.php/topic,10736.0.html
  7. -- and for this plug in :
  8.  
  9. local Version = "1.0 rboer"
  10. local DESCRIPTION = "Smart Meter"
  11. local SM_SID = "urn:rboer-com:serviceId:SmartMeter1"
  12. local EM_SID = "urn:micasaverde-com:serviceId:EnergyMetering1"
  13. local GE_SID = "urn:micasaverde-com:serviceId:GenericSensor1"
  14. local PM_XML = "D_PowerMeter1.xml"
  15. local GE_XML = "D_GenericSensor1.xml"
  16. local THIS_DEVICE = 0
  17. local syslog
  18. local LogLevel = 3
  19. local ShowMultiTariff = 0
  20. local ShowExport = 0
  21. local ShowGas = 0
  22. local ShowWholeHouse = 0
  23. local UseGeneratedPower = 0
  24. local indGasComming = false -- Some meter types split Gas reading over two lines
  25. local powerGeneratorCount = 0
  26. local powerGeneratorDevice = {}
  27.  
  28.  
  29. local Icon = {
  30. Variable = "IconSet", -- Variable controlling the iconsVariable
  31. IDLE = '0', -- No background
  32. OK = '1', -- Green
  33. BUSY = '2', -- Blue
  34. WAIT = '3', -- Amber
  35. ERROR = '4' -- Red
  36. }
  37. local TaskData = {
  38. Description = "Smart Meter",
  39. taskHandle = -1,
  40. ERROR = 2,
  41. ERROR_PERM = -2,
  42. SUCCESS = 4,
  43. BUSY = 1
  44. }
  45.  
  46. ---------------------------------------------------------------------------------------------
  47. -- Utility functions
  48. ---------------------------------------------------------------------------------------------
  49. function log(text, level)
  50. local level = (level or 10)
  51. if (LogLevel >= level) then
  52. if (syslog) then
  53. local slvl
  54. if (level == 1) then slvl = 2
  55. elseif (level == 2) then slvl = 4
  56. elseif (level == 3) then slvl = 5
  57. elseif (level == 4) then slvl = 5
  58. elseif (level == 7) then slvl = 6
  59. elseif (level == 8) then slvl = 6
  60. else slvl = 7
  61. end
  62. syslog:send(text,slvl)
  63. else
  64. if (level == 10) then level = 50 end
  65. print(DESCRIPTION .. ": " .. text or "no text")
  66. end
  67. end
  68. end
  69. -- Get variable value.
  70. -- Use SM_SID and THIS_DEVICE as defaults
  71. local function varGet(name, device, service)
  72. -- local value = luup.variable_get(service or SM_SID, name, tonumber(device or THIS_DEVICE))
  73. return (value or '')
  74. end
  75. -- Update variable when value is different than current.
  76. -- Use SM_SID and THIS_DEVICE as defaults
  77. local function varSet(name, value, device, service)
  78. local service = service or SM_SID
  79. local device = tonumber(device or THIS_DEVICE)
  80. local old = varGet(name, device, service)
  81. if (tostring(value) ~= old) then
  82. -- luup.variable_set(service, name, value, device)
  83. end
  84. end
  85. --get device Variables, creating with default value if non-existent
  86. local function defVar(name, default, device, service)
  87. local service = service or SM_SID
  88. local device = tonumber(device or THIS_DEVICE)
  89. local value = luup.variable_get(service, name, device)
  90. if (not value) then
  91. value = default or '' -- use default value or blank
  92. luup.variable_set(service, name, value, device) -- create missing variable with default value
  93. end
  94. return value
  95. end
  96. -- Set message in task window.
  97. local function task(text, mode)
  98. local mode = mode or TaskData.ERROR
  99. if (mode ~= TaskData.SUCCESS) then
  100. if (mode == TaskData.ERROR_PERM) then
  101. log("task: " .. (text or "no text"), 1)
  102. else
  103. log("task: " .. (text or "no text"))
  104. end
  105. end
  106. TaskData.taskHandle = luup.task(text, (mode == TaskData.ERROR_PERM) and TaskData.ERROR or mode, TaskData.Description, TaskData.taskHandle)
  107. end
  108. -- Set a luup failure message
  109. local function setluupfailure(status,devID)
  110. if (luup.version_major < 7) then status = status ~= 0 end -- fix UI5 status type
  111. luup.set_failure(status,devID)
  112. end
  113. -- Syslog server support. From Netatmo plugin by akbooer
  114. function syslog_server (ip_and_port, tag, hostname)
  115. local sock = socketLib.udp()
  116. local facility = 1 -- 'user'
  117. local emergency, alert, critical, error, warning, notice, info, debug = 0,1,2,3,4,5,6,7
  118. local ip, port = ip_and_port: match "^(%d+%.%d+%.%d+%.%d+):(%d+)$"
  119. if not ip or not port then return nil, "invalid IP or PORT" end
  120. local serialNo = luup.pk_accesspoint
  121. hostname = ("Vera-"..serialNo) or "Vera"
  122. if not tag or tag == '' then tag = DESCRIPTION end
  123. tag = tag:gsub("[^%w]","") or DESCRIPTION -- only alphanumeric, no spaces or other
  124. local function send (self, content, severity)
  125. content = tostring (content)
  126. severity = tonumber (severity) or info
  127. local priority = facility*8 + (severity%8)
  128. local msg = ("<%d>%s %s %s: %s\n"):format (priority, os.date "%b %d %H:%M:%S", hostname, tag, content)
  129. sock:send (msg)
  130. end
  131. local ok, err = sock:setpeername(ip, port)
  132. if ok then ok = {send = send} end
  133. return ok, err
  134. end
  135. -- Set the status Icon
  136. function setStatusIcon(status)
  137. if (status == Icon.OK) then
  138. -- When status is success, then clear after number of seconds
  139. local idleDelay = varGet("OkInterval")
  140. if (tonumber(idleDelay) > 0) then
  141. varSet(Icon.Variable, Icon.OK)
  142. luup.call_delay("idleStatusIcon", idleDelay, "", false)
  143. else
  144. varSet(Icon.Variable, Icon.IDLE)
  145. end
  146. else
  147. varSet(Icon.Variable, status)
  148. end
  149. end
  150. function idleStatusIcon()
  151. local status = varGet(Icon.Variable)
  152. -- When status is success, then clear else do not change
  153. if (status == Icon.OK) then
  154. varSet(Icon.Variable, Icon.IDLE)
  155. end
  156. end
  157. -- Luup Reload function for UI5,6 and 7
  158. local function luup_reload()
  159. if (luup.version_major < 6) then
  160. luup.call_action("urn:micasaverde-com:serviceId:HomeAutomationGateway1", "Reload", {}, 0)
  161. else
  162. luup.reload()
  163. end
  164. end
  165.  
  166. -- Keys for Smart Meter values
  167. local Mt = "/"
  168. local ImpT1 = "1-0:1.8.1"
  169. local ImpT2 = "1-0:1.8.2"
  170. local ExpT1 = "1-0:2.8.1"
  171. local ExpT2 = "1-0:2.8.2"
  172. local ImpWatts = "1-0:1.7.0"
  173. local ExpWatts = "1-0:2.7.0"
  174. local Ta = "0-0:96.14.0"
  175. local Gas2 = "0-1:24.3.0"
  176. local Gas = "0-1:24.2.1"
  177. local WholeHouse = "9-9:9.9.9" -- Dummy Meter reading type
  178. -- Functions to parse meter values from strings
  179. local function SetImpKWH(devID, Key, dataStr)
  180. -- if (devID == nil) then return nil end
  181. local newVal = tonumber(dataStr:match("%d+.%d+"))
  182. local elem = mapperData[Key]
  183. if (newVal ~= elem.val) then
  184. elem.val = newVal
  185. if (ShowMultiTariff == 0) then
  186. -- We are not showing multiple tariffs, so add T1 and T2 KWH for user when both have been read
  187. if (mapperData[ImpT1].val ~= -1) and (mapperData[ImpT2].val ~= -1) then
  188. varSet(elem.var, mapperData[ImpT1].val + mapperData[ImpT2].val, devID, EM_SID)
  189. end
  190. else
  191. varSet(elem.var, newVal, devID, EM_SID)
  192. end
  193. end
  194. return newVal
  195. end
  196. local function SetExpKWH(devID, Key, dataStr)
  197. -- if (devID == nil) then return nil end
  198. local newVal = tonumber(dataStr:match("%d+.%d+"))
  199. local elem = mapperData[Key]
  200. if (newVal ~= elem.val) then
  201. elem.val = newVal
  202. if (ShowMultiTariff == 0) then
  203. -- We are not showing multiple tariffs, so add T1 and T2 KWH for user when both have been read
  204. if (mapperData[ExpT1].val ~= -1) and (mapperData[ExpT2].val ~= -1) then
  205. varSet(elem.var, mapperData[ExpT1].val + mapperData[ExpT2].val, devID, EM_SID)
  206. end
  207. else
  208. varSet(elem.var, newVal, devID, EM_SID)
  209. end
  210. end
  211. return newVal
  212. end
  213. local function SetWatts(devID, Key, dataStr)
  214. -- if (devID == nil) then return nil end
  215. local newVal = tonumber(dataStr:match("%d+.%d+"))*1000
  216. local elem = mapperData[Key]
  217. if (newVal ~= elem.val) then
  218. varSet(elem.var, newVal, devID, EM_SID)
  219. elem.val = newVal
  220. end
  221. return newVal
  222. end
  223. local function SetGas(devID, Key, dataStr)
  224. -- if (devID == nil) then return nil end
  225. local newVal = tonumber(string.match(dataStr:match("(%d+.%d+*m3)"), "%d+.%d+"))
  226. local elem = mapperData[Key]
  227. if (newVal ~= elem.val) then
  228. varSet(elem.var, newVal, devID, GE_SID)
  229. elem.val = newVal
  230. end
  231. return newVal
  232. end
  233. local function SetTarif(devID, Key, dataStr)
  234. local newVal = tonumber(dataStr:match("%d+"))
  235. local elem = mapperData[Key]
  236. if (newVal ~= elem.val) then
  237. elem.val = newVal
  238. if (newVal == 1) then
  239. mapperData[ImpWatts].dev = mapperData[ImpT1].dev
  240. mapperData[ExpWatts].dev = mapperData[ExpT1].dev
  241. varSet(elem.var, 1)
  242. elseif (newVal == 2) then
  243. mapperData[ImpWatts].dev = mapperData[ImpT2].dev
  244. mapperData[ExpWatts].dev = mapperData[ExpT2].dev
  245. varSet(elem.var, 2)
  246. end
  247. end
  248. return newVal
  249. end
  250. local function SetMeter(devID, Key, dataStr)
  251. local elem = mapperData[Key]
  252. if (dataStr ~= elem.val) then
  253. varSet(elem.var, dataStr)
  254. elem.val = dataStr
  255. end
  256. return dataStr
  257. end
  258. local function SetGas2(devID, Key, dataStr)
  259. -- if (mapperData[Gas].dev == nil) then return false end
  260. log ("SetGas2 "..Key.." data "..dataStr)
  261. indGasComming = true
  262. log ("SetGas2 end")
  263. return ""
  264. end
  265. -- Mapping for each key value we are interested in.
  266. mapperData = {
  267. [Mt] = {var = "MeterType", dev = nil, val = "", func = SetMeter, ChildDesc = "", xml = nil},
  268. [Ta] = {var = "ActiveTariff", dev = nil, val = 0, func = SetTarif, ChildDesc = "", xml = nil},
  269. [ImpT1] = {var = "KWH", dev = nil, val = -1, func = SetImpKWH, ChildDesc = "ImportT1", xml = PM_XML},
  270. [ImpT2] = {var = "KWH", dev = nil, val = -1, func = SetImpKWH, ChildDesc = "ImportT2", xml = PM_XML},
  271. [ExpT1] = {var = "KWH", dev = nil, val = -1, func = SetExpKWH, ChildDesc = "ExportT1", xml = PM_XML},
  272. [ExpT2] = {var = "KWH", dev = nil, val = -1, func = SetExpKWH, ChildDesc = "ExportT2", xml = PM_XML},
  273. [ImpWatts] = {var = "Watts", dev = nil, val = 0, func = SetWatts, ChildDesc = "", xml = nil},
  274. [ExpWatts] = {var = "Watts", dev = nil, val = 0, func = SetWatts, ChildDesc = "", xml = nil},
  275. [Gas2]= {var = "N/A", dev = nil, val = 0, func = SetGas2, ChildDesc = "", xml = nil},
  276. [Gas]= {var = "CurrentLevel", dev = nil, val = 0, func = SetGas, ChildDesc = "ExportGas", xml = GE_XML},
  277. [WholeHouse]= {var = "Watts", dev = nil, val = 0, func = nil, ChildDesc = "WholeHouse", xml = PM_XML}
  278. }
  279.  
  280. -- Find the device ID of the type
  281. function findChild(label)
  282. for k, v in pairs(luup.devices) do
  283. if (v.device_num_parent == THIS_DEVICE and v.id == "SM_"..mapperData[label].ChildDesc) then
  284. mapperData[label].dev = k
  285. return true
  286. end
  287. end
  288.  
  289. -- Dump a copy of the Global Module list for debugging purposes.
  290. for k, v in pairs(luup.devices) do
  291. log("Device Number: " .. k ..
  292. " v.device_type: " .. tostring(v.device_type) ..
  293. " v.device_num_parent: " .. tostring(v.device_num_parent) ..
  294. " v.id: " .. tostring(v.id))
  295. end
  296. return false
  297. end
  298.  
  299.  
  300. -- Start up plug in
  301. function SmartMeter_Init(lul_device)
  302. THIS_DEVICE = lul_device
  303. log("Starting SmartMeter device: " .. tostring(THIS_DEVICE),3)
  304. ShowMultiTariff = defVar("ShowMultiTariff",0) -- When 1 show T1 and T2 separately
  305. ShowExport = defVar("ShowExport",0) -- When 1 show Import and Export separately
  306. ShowWholeHouse = defVar("ShowWholeHouse",0) -- When 1 show separate WholeHouse calculation
  307. UseGeneratedPower = defVar("UseGeneratedPower",0) -- When 1 use power generated (e.g. solar) in calculations
  308. local GeneratedPowerSources = defVar("GeneratedPowerSources") -- Device ID list of power generating sources (e.g. solar) in calculations
  309. ShowGas = defVar("ShowGas",0) -- When 1 show Import and Export separately
  310.  
  311. defVar("Gas",0)
  312. mapperData[Ta].val = defVar("ActiveTariff",1)
  313. defVar("MeterType", "Unknown")
  314.  
  315. -- Create the child devices the user wants
  316. local childDevices = luup.chdev.start(THIS_DEVICE);
  317.  
  318. -- Create devices needed if not exist
  319. addMeterDevice(childDevices,ImpT1)
  320. if (ShowMultiTariff == 1) then addMeterDevice(childDevices,ImpT2) end
  321. if (ShowExport == 1) then
  322. addPowerMeterDevice(childDevices,ExpT1)
  323. if (ShowMultiTariff == 1) then addMeterDevice(childDevices,ExpT2) end
  324. end
  325. if (ShowWholeHouse == 1) then addMeterDevice(childDevices,WholeHouse) end
  326. if (ShowGas == 1) then addMeterDevice(childDevices,Gas) end
  327. -- Vera will reload here when there are new devices or changes to a child
  328. luup.chdev.sync(HData.DEVICE, childDevices)
  329.  
  330. -- Pickup device IDs from names
  331. findChild(ImpT1)
  332. if (ShowMultiTariff == 1) then
  333. findChild(ImpT2)
  334. else
  335. mapperData[ImpT2].dev = mapperData[ImpT1].dev
  336. end
  337. if (ShowExport == 1) then
  338. findChild(ExpT1)
  339. if (ShowMultiTariff == 1) then
  340. findChild(ExpT2)
  341. else
  342. mapperData[ExpT2].dev = mapperData[ExpT1].dev
  343. end
  344. end
  345. if (ShowWholeHouse == 1) then findChild(WholeHouse) end
  346. if (ShowGas == 1) then findChild(Gas) end
  347. -- For current watt readings map to right device ID
  348. if (ShowMultiTariff == 1) then
  349. if (mapperData[Ta].val == 1) then
  350. mapperData[ImpWatts].dev = mapperData[ImpT1].dev
  351. if (ShowExport == 1) then mapperData[ExpWatts].dev = mapperData[ExpT1].dev end
  352. else
  353. mapperData[ImpWatts].dev = mapperData[ImpT2].dev
  354. if (ShowExport == 1) then mapperData[ExpWatts].dev = mapperData[ExpT2].dev end
  355. end
  356. else
  357. mapperData[ImpWatts].dev = mapperData[ImpT1].dev
  358. if (ShowExport == 1) then mapperData[ExpWatts].dev = mapperData[ExpT1].dev end
  359. end
  360.  
  361. log("SmartMeter has started...")
  362. setluupfailure(0, THIS_DEVICE)
  363. return true
  364. end
  365.  
  366. -- Find child based on having THIS_DEVICE as parent and the expected altID
  367. function addMeterDevice(childDevices,meterID)
  368. local elem = mapperData[meterID]
  369. local meterName = "SM_"..elem.ChildDesc
  370. local childName = "SmartMeter "..elem.ChildDesc
  371.  
  372. -- Now add the new device to the tree
  373. log("Creating child device id " .. meterName .. " (" .. childName .. ")")
  374. luup.chdev.append(
  375. THIS_DEVICE, -- parent (this device)
  376. childDevices, -- pointer from above "start" call
  377. meterName, -- child Alt ID
  378. childName, -- child device description
  379. "", -- serviceId (keep blank for UI7 restart avoidance)
  380. elem.xml, -- device file for given device
  381. "", -- Implementation file
  382. "", -- parameters to set
  383. true) -- not embedded child devices can go in any room
  384. end
  385.  
  386. ---------------------------------------------------------------------------------------------
  387. -- Data line has been received via serial. Process
  388. ---------------------------------------------------------------------------------------------
  389. function SmartMeter_Incoming(data)
  390. if (data:len() > 0) then
  391. if (indGasComming == true) then
  392. log("indGasComming is true")
  393. -- GAS on ISK5 where GAS reading is on its own line.
  394. local newVal = tonumber(string.match(data, "%d+.%d+"))
  395. log("Gas meter: [" .. newVal .. "]")
  396. local elem = mapperData[Gas]
  397. if (newVal ~= elem.val) then
  398. varSet("CurrentLevel", newVal, elem.dev, GE_SID)
  399. elem.val = newVal
  400. end
  401. indGasComming = false
  402. else
  403. -- Get line key
  404. local Key = data:match("[0-9%:%-%.%/]+")
  405. if Key then
  406. local elem = mapperData[Key]
  407. if elem then
  408. local res, val = pcall(mapperData[Key].func,elem.dev,Key,data:sub(Key:len()+1))
  409. if res then
  410. log("Found key : "..Key.." for "..elem.var.." to set to value "..val)
  411. else
  412. log("Found key : "..Key.." for "..elem.var.." but failed to obtain value ",7)
  413. end
  414. else
  415. log("Not processing : "..data)
  416. end
  417. else
  418. log("No key found in : "..data)
  419. end
  420. end
  421. end
  422. end
  423.  
  424. function SmartMeter_Test()
  425. LogLevel=10
  426. indGasComming=false
  427. -- Meter types
  428. SmartMeter_Incoming("/ISk5\2MT382-1003")
  429. SmartMeter_Incoming("/KFM5KAIFA-METER")
  430. -- ActiveTariff
  431. SmartMeter_Incoming("0-0:96.14.0(0001)")
  432. -- InpT1, InpT2, ExpT1, ExpT2
  433. SmartMeter_Incoming("1-0:1.8.1(000231.094*kWh)")
  434. SmartMeter_Incoming("1-0:1.8.2(000277.981*kWh)")
  435. SmartMeter_Incoming("1-0:2.8.1(000066.164*kWh)")
  436. SmartMeter_Incoming("1-0:2.8.2(000174.446*kWh)")
  437. -- ImpWatts, ExpWatts
  438. SmartMeter_Incoming("1-0:1.7.0(02.104*kW)")
  439. SmartMeter_Incoming("1-0:2.7.0(01.010*kW)")
  440. -- Gas for Kaifa
  441. SmartMeter_Incoming("0-1:24.2.1(140828150000S)(00048.320*m3)")
  442. -- Gas for ISk5
  443. SmartMeter_Incoming("0-1:24.3.0(150301230000)(00)(60)(1)(0-1:24.2.1)(m3)")
  444. SmartMeter_Incoming("(02351.200)" )
  445. SmartMeter_Incoming("0-1:24.4.0(1)" )
  446. SmartMeter_Incoming("0-0:96.1.1(5A424556303035313136323433303132)")
  447. SmartMeter_Incoming("0-1:24.1.0(3)")
  448. SmartMeter_Incoming("0-0:96.13.0()")
  449. SmartMeter_Incoming("0-0:96.3.10(1)")
  450. SmartMeter_Incoming("0-0:17.0.0(0999.00*kW)")
  451. SmartMeter_Incoming("0-1:96.1.0(3238303131303031323439303539333132)")
  452. SmartMeter_Incoming("1-0:31.7.0(000*A)")
  453. SmartMeter_Incoming("1-0:41.7.0(00.080*kW)")
  454. SmartMeter_Incoming("!A89C")
  455. SmartMeter_Incoming("!")
  456. end
  457.  
  458. SmartMeter_Test()
  459.  
  460.  
Success #stdin #stdout 0.01s 5436KB
stdin
Standard input is empty
stdout
Smart Meter: Found key : / for MeterType to set to value ISk5MT382-1003
Smart Meter: Found key : / for MeterType to set to value KFM5KAIFA-METER
Smart Meter: Found key : 0-0:96.14.0 for ActiveTariff to set to value 1
Smart Meter: Found key : 1-0:1.8.1 for KWH to set to value 231.094
Smart Meter: Found key : 1-0:1.8.2 for KWH to set to value 277.981
Smart Meter: Found key : 1-0:2.8.1 for KWH to set to value 66.164
Smart Meter: Found key : 1-0:2.8.2 for KWH to set to value 174.446
Smart Meter: Found key : 1-0:1.7.0 for Watts to set to value 2104.0
Smart Meter: Found key : 1-0:2.7.0 for Watts to set to value 1010.0
Smart Meter: Found key : 0-1:24.2.1 for CurrentLevel to set to value 48.32
Smart Meter: SetGas2 0-1:24.3.0 data (150301230000)(00)(60)(1)(0-1:24.2.1)(m3)
Smart Meter: SetGas2 end
Smart Meter: Found key : 0-1:24.3.0 for N/A to set to value 
Smart Meter: indGasComming is true
Smart Meter: Gas meter: [2351.2]
Smart Meter: Not processing : 0-1:24.4.0(1)
Smart Meter: Not processing : 0-0:96.1.1(5A424556303035313136323433303132)
Smart Meter: Not processing : 0-1:24.1.0(3)
Smart Meter: Not processing : 0-0:96.13.0()
Smart Meter: Not processing : 0-0:96.3.10(1)
Smart Meter: Not processing : 0-0:17.0.0(0999.00*kW)
Smart Meter: Not processing : 0-1:96.1.0(3238303131303031323439303539333132)
Smart Meter: Not processing : 1-0:31.7.0(000*A)
Smart Meter: Not processing : 1-0:41.7.0(00.080*kW)
Smart Meter: Not processing : !A89C
Smart Meter: No key found in : !