Released version 3-2015061300
[notion.git] / contrib / statusd / statusd_laptopstatus.lua
blobcee89dfdc2e89e290ce5a06cf655e2c549e2d634
1 -- Authors: Jari Eskelinen <jari.eskelinen@iki.fi>, René van Bevern <rvb@pro-linux.de>, Juri Hamburg <juri@fail2fail.com>, Philipp Hartwig <ph@phhart.de>
2 -- License: GPL, version 2
3 -- Last Changed: 2013-04-09
4 --
5 -- statusd_laptopstatus.lua v0.0.3 (last modified 2011-10-30)
6 --
7 -- Copyright (C) 2005 Jari Eskelinen <jari.eskelinen@iki.fi>
8 -- modified by René van Bevern <rvb@pro-linux.de> for error handling
9 -- modified by Juri Hamburg <juri@fail2fail.com> for sysfs support (2011)
10 -- modified by Philipp Hartwig <ph@phhart.de> for improved sysfs support (2013)
12 -- Permission to copy, redistirbute, modify etc. is granted under the terms
13 -- of GNU GENERAL PUBLIC LICENSE Version 2.
15 -- This is script for displaying some interesting information about your
16 -- laptops power saving in Notion's status monitor. Script is very Linux
17 -- specific (uses procfs or sysfs interface) and needs ACPI-support (don't
18 -- know exactly but 2.6.x kernels should be fine). Also if you have some
19 -- kind of exotic hardware (multiple processors, multiple batteries etc.)
20 -- this script probably will fail or show incorrect information.
22 -- The script will try sysfs interface first. If that fails, it will try to
23 -- use the procfs interface.
25 -- Just throw this script under ~/.notion and add following keywords to your
26 -- cfg_statusbar.lua's template-line with your own taste:
28 -- %laptopstatus_cpuspeed
29 -- %laptopstatus_temperature
30 -- %laptopstatus_batterypercent
31 -- %laptopstatus_batterytimeleft
32 -- %laptopstatus_batterydrain
33 --
34 -- Template example: template="[ %date || load:% %>load || CPU: %laptopstatus_cpuspeed %laptopstatus_temperature || BATT: %laptopstatus_batterypercent %laptopstatus_batterytimeleft %laptopstatus_batterydrain ]"
36 -- You can also run this script with lua interpreter and see if you get
37 -- right values.
39 -- TODO: * Is it stupid to use file:read("*all"), can this cause infinite
40 -- loops in some weird situations?
41 -- * Do not poll for information not defined in template to be used
44 -- SETTINGS
47 NA = "n/a"
48 AC = "*AC*"
50 sys_dir="/sys/class/"
51 proc_dir="/proc/acpi/"
53 if not statusd_laptopstatus then
54 statusd_laptopstatus = {
55 interval = 10, -- Polling interval in seconds
56 temperature_important = 66, -- Temperature which will cause important hint
57 temperature_critical = 71, -- Temperature which will cause critical hint
58 batterypercent_important = 10, -- Battery percent which will cause important hint
59 batterypercent_critical = 5, -- Battery percent which will cause critical hint
60 batterytimeleft_important = 600, -- Battery time left (in secs) which will cause important hint
61 batterytimeleft_critical = 300, -- Battery time left (in secs) which will cause critical hint
63 end
65 if statusd ~= nil then
66 statusd_laptopstatus=table.join(statusd.get_config("laptopstatus"), statusd_laptopstatus)
67 end
70 -- CODE
72 local laptopstatus_timer
73 local RingBuffer = {}
75 RingBuffer.__index = RingBuffer
77 function RingBuffer.create(size)
78 local buf = {}
79 setmetatable(buf,RingBuffer)
80 buf.size = size
81 buf.elements = {}
82 buf.current = 1
83 return buf
84 end
86 function RingBuffer:add(val)
87 if self.current > self.size then
88 self.current = 1
89 end
90 self.elements[self.current] = val
91 self.current = self.current + 1
92 end
94 function average(array)
95 local sum = 0
96 for i,v in ipairs(array) do
97 sum = sum + v
98 end
99 if #array ~= 0 then
100 return sum / #array
101 else
102 return 0
106 rates_buffer = RingBuffer.create(20)
109 function read_val(file)
110 local fd = io.open(file)
111 if fd then
112 local value = fd:read("*a")
113 fd:close()
114 return value
118 -- Returns the full path of the first subdirectory of "path" containing a file
119 -- with name "filename" and first line equal to "content"
120 function lookup_subdir(path, filename, content)
121 if path:sub(-1) ~= "/" then path = path .. "/" end
122 local fd=io.open(path)
123 if not fd then return nil end
124 fd:close()
126 function dir_iter()
127 if pcall(require, "lfs") then
128 return lfs.dir(path)
129 else
130 return io.popen("ls -1 " .. path):lines()
134 for dir in dir_iter() do
135 if not (dir == "." or dir == "..") then
136 local fd=io.open(path .. dir .. "/" .. filename)
137 if fd and ((not content) or content == fd:read():lower()) then
138 return path .. dir .. "/"
144 local function get_cpu()
145 local mhz, hint
146 if pcall(function ()
147 local status
148 status, _, mhz = string.find(read_val("/proc/cpuinfo"),
149 "cpu MHz%s+: (%d+)")
150 if not status then error("could not get MHz") end
151 end)
152 then return {speed=string.format("%4dMHz", math.ceil(mhz/5)*5),
153 hint=hint}
154 else return {speed=NA, hint=hint} end
157 local function on_ac()
158 if ac_sys then
159 return (tonumber(read_val(ac_dir .. "online") or "") == 1)
160 else
161 return string.find(read_val(ac_dir .. "state") or "", "state:%s+on.line")
165 local function get_thermal_sysfs()
166 local temp, hint = nil, "normal"
168 if pcall(function ()
169 temp=read_val(temp_dir .. "temp")
170 temp = tonumber(temp)/1000
171 end)
172 then if temp >= statusd_laptopstatus.temperature_important then
173 hint = "important" end
174 if temp >= statusd_laptopstatus.temperature_critical then
175 hint = "critical" end
176 return {temperature=string.format("%02dC", temp), hint=hint}
177 else return {temperature=NA, hint = "hint"} end
180 local function get_thermal_procfs()
181 local temp, hint = nil, "normal"
183 if pcall(function ()
184 local status
185 status, _, temp = string.find(read_val(temp_dir .. "temperature"),
186 "temperature:%s+(%d+).*")
187 if not status then error("could not get temperature") end
188 temp = tonumber(temp)
189 end)
190 then if temp >= statusd_laptopstatus.temperature_important then
191 hint = "important" end
192 if temp >= statusd_laptopstatus.temperature_critical then
193 hint = "critical" end
194 return {temperature=string.format("%02dC", temp), hint=hint}
195 else return {temperature=NA, hint = "hint"} end
198 local function get_thermal()
199 if temp_sys then
200 return get_thermal_sysfs()
201 else
202 return get_thermal_procfs()
207 local rate_sysfs
208 local last_power
210 local function get_battery_sysfs()
211 local percenthint = "normal"
212 local timelefthint = "normal"
213 local full, now
215 if pcall(function ()
216 now = tonumber(read_val(bat_dir .. "energy_now"))
217 full = tonumber(read_val(bat_dir .. "energy_full"))
218 end)
220 pcall(function ()
221 now = tonumber(read_val(bat_dir .. "charge_now"))
222 full = tonumber(read_val(bat_dir .. "charge_full"))
223 end)
224 then
225 --Some batteries erronously report a charge_now value of charge_full_design when full.
226 if now > full then now=full end
227 local percent = math.floor(now / full * 100 + 5/10)
228 local timeleft
229 if on_ac() then
230 timeleft = AC
231 elseif last_power ~= nil and now < last_power then
232 rate_sysfs = last_power - now
233 rates_buffer:add(rate_sysfs)
234 secs = statusd_laptopstatus.interval * (now / average(rates_buffer.elements))
235 mins = secs / 60
236 hours = math.floor(mins / 60)
237 mins = math.floor(mins - (hours * 60))
238 timeleft = string.format("%02d:%02d", hours, mins)
239 else
240 timeleft = NA
241 end
242 last_power = now
244 if percent <= statusd_laptopstatus.batterypercent_important then percenthint = "important" end
245 if percent <= statusd_laptopstatus.batterypercent_critical then percenthint = "critical" end
246 return { percent=string.format("%02d%%", percent), timeleft=timeleft, drain=NA, percenthint=percenthint, timelefthint=timelefthint}
247 else return { percent=NA, timeleft=NA, drain=NA, percenthint=percenthint, timelefthint=timelefthint} end
250 local function get_battery_procfs()
251 local percenthint = "normal"
252 local timelefthint = "normal"
253 local lastfull, rate, rateunit, remaining
255 if pcall(function ()
256 local status
257 local statecontents = read_val(bat_dir .. "state")
259 status, _, lastfull = string.find(read_val(bat_dir .. "info"), "last full capacity:%s+(%d+).*")
260 if not status then error("could not get full battery capacity") end
261 lastfull = tonumber(lastfull)
262 if string.find(statecontents, "present rate:%s+unknown.*") then
263 rate = -1
264 else
265 status, _, rate, rateunit = string.find(statecontents, "present rate:%s+(%d+)(.*)")
266 if not status then error("could not get battery draining-rate") end
267 rate = tonumber(rate)
269 status, _, remaining = string.find(statecontents, "remaining capacity:%s+(%d+).*")
270 if not status then error("could not get remaining capacity") end
271 remaining = tonumber(remaining)
272 end) then
273 local percent = math.floor(remaining / lastfull * 100 + 5/10)
274 local timeleft
275 local hours, secs, mins
276 if on_ac() then
277 timeleft = AC
278 elseif rate <= 0 then
279 timeleft = NA
280 else
281 secs = 3600 * (remaining / rate)
282 mins = secs / 60
283 hours = math.floor(mins / 60)
284 mins = math.floor(mins - (hours * 60))
285 timeleft = string.format("%02d:%02d", hours, mins)
288 if secs ~= nil and secs <= statusd_laptopstatus.batterytimeleft_important then timelefthint = "important" end
289 if secs ~= nil and secs <= statusd_laptopstatus.batterytimeleft_critical then timelefthint = "critical" end
290 if percent <= statusd_laptopstatus.batterypercent_important then percenthint = "important" end
291 if percent <= statusd_laptopstatus.batterypercent_critical then percenthint = "critical" end
293 return { percent=string.format("%02d%%", percent), timeleft=timeleft, drain=tostring(rate)..rateunit, percenthint=percenthint, timelefthint=timelefthint}
294 else return { percent=NA, timeleft=NA, drain=NA, percenthint=percenthint, timelefthint=timelefthint} end
297 local function get_battery()
298 if bat_sys then
299 return get_battery_sysfs()
300 else
301 return get_battery_procfs()
305 local last_timeleft = nil
307 function do_init()
308 init=true
310 ac_dir=lookup_subdir(sys_dir .. "power_supply" , "type", "mains")
311 ac_sys=true
312 if(not ac_dir) then
313 ac_dir=lookup_subdir(proc_dir .. "ac_adapter", "state")
314 ac_sys=false
317 bat_dir=lookup_subdir(sys_dir .. "power_supply", "type", "battery")
318 bat_sys=true
319 if(not bat_dir) then
320 bat_dir=lookup_subdir(proc_dir .. "battery", "state")
321 bat_sys=false
324 temp_dir=lookup_subdir(sys_dir .. "thermal", "type", "acpitz") or lookup_subdir(sys_dir .. "thermal", "type", "thermal zone")
325 temp_sys=true
326 if(not temp_dir) then
327 temp_dir=lookup_subdir(proc_dir .. "thermal_zone", "temperature")
328 temp_sys=false
332 local function update_laptopstatus ()
333 if not init then do_init() end
334 cpu = get_cpu()
335 thermal = get_thermal()
336 battery = get_battery()
338 -- Informing statusd OR printing to stdout if statusd not present
339 if statusd ~= nil then
340 statusd.inform("laptopstatus_cpuspeed", cpu.speed)
341 statusd.inform("laptopstatus_cpuspeed_template", "xxxxMHz")
342 statusd.inform("laptopstatus_cpuspeed_hint", cpu.hint)
343 statusd.inform("laptopstatus_temperature", thermal.temperature)
344 statusd.inform("laptopstatus_temperature_template", "xxxC")
345 statusd.inform("laptopstatus_temperature_hint", thermal.hint)
346 statusd.inform("laptopstatus_batterypercent", battery.percent)
347 statusd.inform("laptopstatus_batterypercent_template", "xxx%")
348 statusd.inform("laptopstatus_batterypercent_hint", battery.percenthint)
349 if battery.timeleft ~= NA or last_timeleft == AC then
350 statusd.inform("laptopstatus_batterytimeleft", battery.timeleft)
351 last_timeleft = battery.timeleft
353 statusd.inform("laptopstatus_batterytimeleft_template", "hh:mm")
354 statusd.inform("laptopstatus_batterytimeleft_hint", battery.timelefthint)
355 statusd.inform("laptopstatus_batterydrain", battery.drain)
356 laptopstatus_timer:set(statusd_laptopstatus.interval*1000, update_laptopstatus)
357 else
358 io.stdout:write("CPU: "..cpu.speed.." "..thermal.temperature.." || BATT: "..battery.percent.." "..battery.timeleft.."\n")
363 if statusd ~= nil then
364 laptopstatus_timer = statusd.create_timer()
366 update_laptopstatus()