1if not modules then modules = { } end modules ['util-you'] = {
2 version = 1.002,
3 comment = "library for fetching data from youless kwh meter polling device",
4 author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5 copyright = "PRAGMA ADE",
6 license = "see context related readme files"
7}
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23require("util-jsn")
24
25
26
27utilities = utilities or { }
28local youless = { }
29utilities.youless = youless
30
31local lpegmatch = lpeg.match
32local formatters = string.formatters
33
34local tonumber, type, next = tonumber, type, next
35
36local round, div = math.round, math.div
37local osdate, ostime = os.date, os.time
38
39local report = logs.reporter("youless")
40local trace = false
41
42
43
44local http = socket.http
45
46
47
48local f_password = formatters["http://%s/L?w=%s"]
49
50local f_fetchers = {
51 electricity = formatters["http://%s/V?%s=%i&f=j"],
52 gas = formatters["http://%s/W?%s=%i&f=j"],
53 pulse = formatters["http://%s/Z?%s=%i&f=j"],
54}
55
56local function fetch(url,password,what,i,category)
57 local fetcher = f_fetchers[category or "electricity"]
58 if not fetcher then
59 report("invalid fetcher %a",category)
60 else
61 local url = fetcher(url,what,i)
62 local data, h = http.request(url)
63 local result = data and utilities.json.tolua(data)
64 return result
65 end
66end
67
68
69
70local tovalue = lpeg.Cs((lpeg.R("09") + lpeg.P(1)/"")^1) / tonumber
71
72
73
74local totime = (lpeg.C(4) / tonumber) * lpeg.P("-")
75 * (lpeg.C(2) / tonumber) * lpeg.P("-")
76 * (lpeg.C(2) / tonumber) * lpeg.P("T")
77 * (lpeg.C(2) / tonumber) * lpeg.P(":")
78 * (lpeg.C(2) / tonumber) * lpeg.P(":")
79 * (lpeg.C(2) / tonumber)
80
81local function collapsed(data,dirty)
82 for list, parent in next, dirty do
83 local t, n = { }, { }
84 for k, v in next, list do
85 local d = div(k,10) * 10
86 t[d] = (t[d] or 0) + v
87 n[d] = (n[d] or 0) + 1
88 end
89 for k, v in next, t do
90 t[k] = round(t[k]/n[k])
91 end
92 parent[1][parent[2]] = t
93 end
94 return data
95end
96
97local function get(url,password,what,step,data,option,category)
98 if not data then
99 data = { }
100 end
101 local dirty = { }
102 while true do
103 local d = fetch(url,password,what,step,category)
104 local v = d and d.val
105 if v and #v > 0 then
106 local c_year, c_month, c_day, c_hour, c_minute, c_seconds = lpegmatch(totime,d.tm)
107 if c_year and c_seconds then
108 local delta = tonumber(d.dt)
109 local tnum = ostime {
110 year = c_year,
111 month = c_month,
112 day = c_day,
113 hour = c_hour,
114 min = c_minute,
115 sec = c_seconds,
116 }
117 for i=1,#v do
118 local vi = v[i]
119 if vi ~= "*" then
120 local newvalue = lpegmatch(tovalue,vi)
121 if newvalue then
122 local t = tnum + (i-1)*delta
123
124
125 local c = osdate("*t",tnum + (i-1)*delta)
126 local c_year = c.year
127 local c_month = c.month
128 local c_day = c.day
129 local c_hour = c.hour
130 local c_minute = c.min
131 local c_seconds = c.sec
132 if c_year and c_seconds then
133 local years = data.years if not years then years = { } data.years = years end
134 local d_year = years[c_year] if not d_year then d_year = { } years[c_year] = d_year end
135 local months = d_year.months if not months then months = { } d_year.months = months end
136 local d_month = months[c_month] if not d_month then d_month = { } months[c_month] = d_month end
137 local days = d_month.days if not days then days = { } d_month.days = days end
138 local d_day = days[c_day] if not d_day then d_day = { } days[c_day] = d_day end
139 if option == "average" or option == "total" then
140 if trace then
141 local oldvalue = d_day[option]
142 if oldvalue and oldvalue ~= newvalue then
143 report("category %s, step %i, time %s: old %s %s updated to %s",category,step,osdate("%Y-%m-%dT%H:%M:%S",t),option,oldvalue,newvalue)
144 end
145 end
146 d_day[option] = newvalue
147 elseif option == "value" then
148 local hours = d_day.hours if not hours then hours = { } d_day.hours = hours end
149 local d_hour = hours[c_hour] if not d_hour then d_hour = { } hours[c_hour] = d_hour end
150 if trace then
151 local oldvalue = d_hour[c_minute]
152 if oldvalue and oldvalue ~= newvalue then
153 report("category %s, step %i, time %s: old %s %s updated to %s",category,step,osdate("%Y-%m-%dT%H:%M:%S",t),"value",oldvalue,newvalue)
154 end
155 end
156 d_hour[c_minute] = newvalue
157 if not dirty[d_hour] then
158 dirty[d_hour] = { hours, c_hour }
159 end
160 else
161
162 end
163 end
164 end
165 end
166 end
167 end
168 else
169 return collapsed(data,dirty)
170 end
171 step = step + 1
172 end
173 return collapsed(data,dirty)
174end
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195function youless.collect(specification)
196 if type(specification) ~= "table" then
197 return
198 end
199 local host = specification.host or ""
200 local data = specification.data or { }
201 local filename = specification.filename or ""
202 local variant = specification.variant or "kwh"
203 local detail = specification.detail or false
204 local nobackup = specification.nobackup or false
205 local password = specification.password or ""
206 local oldstuff = false
207 if host == "" then
208 return
209 end
210 if filename == "" then
211 return
212 else
213 data = table.load(filename) or data
214 end
215 if variant == "electricity" then
216 get(host,password,"m",1,data,"total","electricity")
217 if oldstuff then
218 get(host,password,"d",1,data,"average","electricity")
219 end
220 get(host,password,"w",1,data,"value","electricity")
221 if detail then
222 get(host,password,"h",1,data,"value","electricity")
223 end
224 elseif variant == "pulse" then
225
226
227
228 get(host,password,"m",1,data,"total","pulse")
229 if oldstuff then
230 get(host,password,"d",1,data,"average","pulse")
231 end
232 detail = true
233 get(host,password,"w",1,data,"value","pulse")
234 if detail then
235 get(host,password,"h",1,data,"value","pulse")
236 end
237 elseif variant == "gas" then
238 get(host,password,"m",1,data,"total","gas")
239 if oldstuff then
240 get(host,password,"d",1,data,"average","gas")
241 end
242 get(host,password,"w",1,data,"value","gas")
243 if detail then
244 get(host,password,"h",1,data,"value","gas")
245 end
246 else
247 return
248 end
249 local path = file.dirname(filename)
250 local base = file.basename(filename)
251 data.variant = variant
252 data.host = host
253 data.updated = os.now()
254 if nobackup then
255
256 local tempname = file.join(path,"youless.tmp")
257 table.save(tempname,data)
258 local check = table.load(tempname)
259 if type(check) == "table" then
260 local keepname = file.replacesuffix(filename,"old")
261 os.remove(keepname)
262 if lfs.isfile(keepname) then
263 report("error in removing %a",keepname)
264 else
265 os.rename(filename,keepname)
266 os.rename(tempname,filename)
267 end
268 else
269 report("error in saving %a",tempname)
270 end
271 else
272 local keepname = file.join(path,formatters["%s-%s"](os.date("%Y-%m-%d-%H-%M-%S",os.time()),base))
273 os.rename(filename,keepname)
274 if lfs.isfile(filename) then
275 report("error in renaming %a",filename)
276 else
277 table.save(filename,data)
278 end
279 end
280 return data
281end
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312function youless.analyze(data)
313 if type(data) == "string" then
314 data = table.load(data)
315 end
316 if type(data) ~= "table" then
317 return false, "no data"
318 end
319 if not data.years then
320 return false, "no years"
321 end
322 local variant = data.variant
323 local unit, maxunit
324 if variant == "electricity" or variant == "watt" then
325 unit = "watt"
326 maxunit = "maxwatt"
327 elseif variant == "gas" then
328 unit = "liters"
329 maxunit = "maxliters"
330 elseif variant == "pulse" then
331 unit = "watt"
332 maxunit = "maxwatt"
333 else
334 return false, "invalid variant"
335 end
336 for y, year in next, data.years do
337 local a_year, n_year, m_year = 0, 0, 0
338 if year.months then
339 for m, month in next, year.months do
340 local a_month, n_month = 0, 0
341 if month.days then
342 for d, day in next, month.days do
343 local a_day, n_day = 0, 0
344 if day.hours then
345 for h, hour in next, day.hours do
346 local a_hour, n_hour, m_hour = 0, 0, 0
347 for k, v in next, hour do
348 if type(k) == "number" then
349 a_hour = a_hour + v
350 n_hour = n_hour + 1
351 if v > m_hour then
352 m_hour = v
353 end
354 end
355 end
356 n_day = n_day + n_hour
357 a_day = a_day + a_hour
358 hour[maxunit] = m_hour
359 hour[unit] = a_hour / n_hour
360 if m_hour > m_year then
361 m_year = m_hour
362 end
363 end
364 end
365 if n_day > 0 then
366 a_month = a_month + a_day
367 n_month = n_month + n_day
368 day[unit] = a_day / n_day
369 else
370 day[unit] = 0
371 end
372 end
373 end
374 if n_month > 0 then
375 a_year = a_year + a_month
376 n_year = n_year + n_month
377 month[unit] = a_month / n_month
378 else
379 month[unit] = 0
380 end
381 end
382 end
383 if n_year > 0 then
384 year[unit] = a_year / n_year
385 year[maxunit] = m_year
386 else
387 year[unit] = 0
388 year[maxunit] = 0
389 end
390 end
391 return data
392end
393 |