Cleanup config.nodes_of
[check_mk.git] / checks / temperature.include
blob903c908d804d8f64ed30868ac354ecbf40fd40bc
1 #!/usr/bin/python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
28 def fahrenheit_to_celsius(tempf, relative=False):
29 if tempf is None:
30 return None
32 if relative:
33 return float(tempf) * (5.0 / 9.0)
34 return (float(tempf) - 32) * (5.0 / 9.0)
37 def celsius_to_fahrenheit(tempc, relative=False):
38 if tempc is None:
39 return None
41 if relative:
42 return float(tempc) * (9.0 / 5.0)
43 return (float(tempc) * (9.0 / 5.0)) + 32
46 def from_celsius(tempc, unit, relative=False):
47 if unit == "f":
48 return celsius_to_fahrenheit(tempc, relative)
49 elif unit == "k":
50 if relative:
51 return tempc
52 return tempc + 273.15
53 return tempc
56 def to_celsius(reading, unit, relative=False):
57 if isinstance(reading, tuple):
58 return tuple([to_celsius(x, unit, relative) for x in reading])
59 elif unit == "f":
60 return fahrenheit_to_celsius(reading, relative)
61 elif unit == "k":
62 if relative:
63 return reading
64 elif reading is None:
65 return None
66 return reading - 273.15
67 return reading
70 # Format number according to its datatype
71 def render_temp(n, output_unit, relative=False):
72 t = from_celsius(n, output_unit, relative)
73 if isinstance(n, int):
74 return "%d" % t
75 return "%.1f" % t
78 temp_unitsym = {
79 "c": u"°C",
80 "f": u"°F",
81 "k": "K",
85 def check_temperature_determine_levels(dlh, usr_warn, usr_crit, usr_warn_lower, usr_crit_lower,
86 dev_warn, dev_crit, dev_warn_lower, dev_crit_lower,
87 dev_unit):
88 # min that deals correctly with None
89 def minn(a, b):
90 return min(a, b) or a or b
92 # Ignore device's own levels
93 if dlh == "usr":
94 warn, crit, warn_lower, crit_lower = usr_warn, usr_crit, usr_warn_lower, usr_crit_lower
96 # Only use device's levels, ignore yours
97 elif dlh == "dev":
98 warn, crit, warn_lower, crit_lower = dev_warn, dev_crit, dev_warn_lower, dev_crit_lower
100 # The following four cases are all identical, if either *only* device levels or *only*
101 # user levels exist (or no levels at all).
103 # Use least critical of your and device's levels. If just one of both is defined,
104 # take that. max deals correctly with None here. min does not work because None < int.
105 # minn is a min that deals with None in the way we want here.
106 elif dlh == "best":
107 warn, crit = max(usr_warn, dev_warn), max(usr_crit, dev_crit)
108 warn_lower, crit_lower = minn(usr_warn_lower, dev_warn_lower), minn(
109 usr_crit_lower, dev_crit_lower)
111 # Use most critical of your and device's levels
112 elif dlh == "worst":
113 warn, crit = minn(usr_warn, dev_warn), minn(usr_crit, dev_crit)
114 warn_lower, crit_lower = max(usr_warn_lower, dev_warn_lower), max(
115 usr_crit_lower, dev_crit_lower)
117 # Use user's levels if present, otherwise the device's
118 elif dlh == "usrdefault":
119 if usr_warn is not None and usr_crit is not None:
120 warn, crit = usr_warn, usr_crit
121 else:
122 warn, crit = dev_warn, dev_crit
123 if usr_warn_lower is not None and usr_crit_lower is not None:
124 warn_lower, crit_lower = usr_warn_lower, usr_crit_lower
125 else:
126 warn_lower, crit_lower = dev_warn_lower, dev_crit_lower
128 # Use device's levels if present, otherwise yours
129 elif dlh == "devdefault":
130 if dev_warn is not None and dev_crit is not None:
131 warn, crit = dev_warn, dev_crit
132 else:
133 warn, crit = usr_warn, usr_crit
135 if dev_warn_lower is not None and dev_crit_lower is not None:
136 warn_lower, crit_lower = dev_warn_lower, dev_crit_lower
137 else:
138 warn_lower, crit_lower = usr_warn_lower, usr_crit_lower
140 return warn, crit, warn_lower, crit_lower
143 # determine temperature trends. This is a private function, not to be called by checks
144 def check_temperature_trend(temp, params, output_unit, crit, crit_lower, unique_name):
145 def combiner(status, infotext):
146 if "status" in dir(combiner):
147 combiner.status = max(combiner.status, status)
148 else:
149 combiner.status = status
151 if "infotext" in dir(combiner):
152 combiner.infotext += ", " + infotext
153 else:
154 combiner.infotext = infotext
156 try:
157 trend_range_min = params["period"]
158 this_time = time.time()
160 # first compute current rate in C/s by computing delta since last check
161 rate = get_rate("temp.%s.delta" % unique_name, this_time, temp, True)
163 # average trend, initialize with zero, rate_avg is in C/s
164 rate_avg = get_average("temp.%s.trend" % unique_name, this_time, rate, trend_range_min,
165 True)
167 # rate_avg is growth in C/s, trend is in C per trend range minutes
168 trend = float(rate_avg * trend_range_min * 60.0)
169 sign = "+" if trend > 0 else ""
170 combiner(0, "rate: %s%s/%g min" %\
171 (sign, render_temp(trend, output_unit, True), trend_range_min))
173 if "trend_levels" in params:
174 warn_upper_trend, crit_upper_trend = params["trend_levels"]
175 else:
176 warn_upper_trend = crit_upper_trend = None
177 # it may be unclear to the user if he should specify temperature decrease as a negative
178 # number or positive. This works either way. Having a positive lower bound makes no
179 # sense anyway.
180 if "trend_levels_lower" in params:
181 warn_lower_trend, crit_lower_trend =\
182 [abs(x) * -1 for x in params["trend_levels_lower"]]
183 else:
184 warn_lower_trend = crit_lower_trend = None
186 if crit_upper_trend is not None and trend > crit_upper_trend:
187 combiner(2, u"rising faster than %s/%g min(!!)" %\
188 (render_temp(crit_upper_trend, output_unit, True), trend_range_min))
189 elif warn_upper_trend is not None and trend > warn_upper_trend:
190 combiner(1, u"rising faster than %s/%g min(!)" %\
191 (render_temp(warn_upper_trend, output_unit, True), trend_range_min))
192 elif crit_lower_trend is not None and trend < crit_lower_trend:
193 combiner(2, u"falling faster than %s/%g min(!!)" %\
194 (render_temp(crit_lower_trend, output_unit, True), trend_range_min))
195 elif warn_lower_trend is not None and trend < warn_lower_trend:
196 combiner(1, u"falling faster than %s/%g min(!)" %\
197 (render_temp(warn_lower_trend, output_unit, True), trend_range_min))
199 if "trend_timeleft" in params:
200 # compute time until temperature limit is reached
201 # The start value of minutes_left is negative. The pnp graph and the perfometer
202 # will interpret this as inifinite -> not growing
203 minutes_left = -1
204 limit = crit if trend > 0 else crit_lower
206 if limit: # crit levels may not be set, especially lower level
207 diff_to_limit = limit - temp
208 if rate_avg != 0.0:
209 minutes_left = (diff_to_limit / rate_avg) / 60.0
210 else:
211 minutes_left = float("inf")
213 def format_minutes(minutes):
214 if minutes > 60: # hours
215 hours = minutes / 60
216 minutes += -int(hours) * 60
217 return "%dh %02dm" % (hours, minutes)
218 return "%d minutes" % minutes
220 warn, crit = params["trend_timeleft"]
221 if minutes_left <= crit:
222 combiner(2, "%s until temp limit reached(!!)" % format_minutes(minutes_left))
223 elif minutes_left <= warn:
224 combiner(1, "%s until temp limit reached(!)" % format_minutes(minutes_left))
225 except MKCounterWrapped:
226 pass
227 return combiner.status, combiner.infotext
230 # Checks Celsius temperature against crit/warn levels defined in params. temp must
231 # be int or float. Parameters:
232 # reading: temperature reading of the device (per default interpreted as Celsius)
233 # params: check parameters (pair or dict)
234 # unique_name: unique name of this check, used for counters
235 # dev_unit: unit of the device reading if this is not Celsius ("f": Fahrenheit, "k": Kelvin)
236 # dev_levels: warn/crit levels of the device itself, if any. In the same unit as temp (dev_unit)
237 # dev_level_lower: lower warn/crit device levels
238 # dev_status: temperature state (0, 1, 2) as the device reports it (if applies)
239 # dev_status_name: the device name (will be added in the check output)
240 # Note: you must not specify dev_status and dev_levels at the same time!
243 def check_temperature(reading,
244 params,
245 unique_name,
246 dev_unit="c",
247 dev_levels=None,
248 dev_levels_lower=None,
249 dev_status=None,
250 dev_status_name=None):
251 def check_temp_levels(temp, warn, crit, warn_lower, crit_lower):
252 if crit is not None and temp >= crit:
253 status = 2
254 elif crit_lower is not None and temp < crit_lower:
255 status = 2
256 elif warn is not None and temp >= warn:
257 status = 1
258 elif warn_lower is not None and temp < warn_lower:
259 status = 1
260 else:
261 status = 0
262 return status
264 # Convert legacy tuple params into new dict
265 if params is None or params == (None, None):
266 params = {}
267 elif isinstance(params, tuple):
268 params = {"levels": params}
270 # Convert reading into Celsius
271 input_unit = params.get("input_unit", dev_unit)
272 output_unit = params.get("output_unit", "c")
273 temp = to_celsius(reading, input_unit)
275 # Prepare levels, dealing with user defined and device's own levels
276 usr_levels = params.get("levels")
277 usr_levels_lower = params.get("levels_lower")
279 # Set all user levels to None. None means do not impose a level
280 usr_warn, usr_crit = usr_levels or (None, None)
281 usr_warn_lower, usr_crit_lower = usr_levels_lower or (None, None)
283 # Same for device levels
284 dev_warn, dev_crit = to_celsius(dev_levels or (None, None), dev_unit)
285 dev_warn_lower, dev_crit_lower = to_celsius(dev_levels_lower or (None, None), dev_unit)
287 # Decide which of user's and device's levels should be used according to the setting
288 # "device_levels_handling". Result is four variables: {warn,crit}{,_lower}
289 dlh = params.get("device_levels_handling", "usrdefault")
291 warn, crit, warn_lower, crit_lower =\
292 check_temperature_determine_levels(dlh, usr_warn, usr_crit,
293 usr_warn_lower, usr_crit_lower,
294 dev_warn, dev_crit,
295 dev_warn_lower, dev_crit_lower, dev_unit)
297 if dlh == "usr" or (dlh == "userdefault" and usr_levels):
298 # ignore device status if user-levels are used
299 dev_status = None
301 # Now finally compute status. Hooray!
302 status = check_temp_levels(temp, warn, crit, warn_lower, crit_lower)
303 if dev_status is not None:
304 if dlh == "best":
305 status = min(status, dev_status)
306 else:
307 status = max(status, dev_status)
309 perfdata = [("temp", temp, warn, crit)]
311 # Render actual temperature, e.g. "17.8 °F"
312 infotext = "%s %s" % (render_temp(temp, output_unit), temp_unitsym[output_unit])
314 if dev_status is not None and dev_status != 0 and dev_status_name: # omit status in OK case
315 infotext += ", %s" % dev_status_name
317 # In case of a non-OK status output the information about the levels
318 if status != 0:
319 usr_levelstext = ""
320 usr_levelstext_lower = ""
321 dev_levelstext = ""
322 dev_levelstext_lower = ""
324 if usr_levels:
325 usr_levelstext = " (warn/crit at %s/%s %s)" % (render_temp(usr_warn, output_unit),
326 render_temp(usr_crit, output_unit),
327 temp_unitsym[output_unit])
329 if usr_levels_lower:
330 usr_levelstext_lower = " (warn/crit below %s/%s %s)" % (render_temp(
331 usr_warn_lower, output_unit), render_temp(usr_crit_lower, output_unit),
332 temp_unitsym[output_unit])
334 if dev_levels:
335 dev_levelstext = " (device warn/crit at %s/%s %s)" % (render_temp(
336 dev_warn, output_unit), render_temp(dev_crit, output_unit),
337 temp_unitsym[output_unit])
339 if dev_levels_lower:
340 dev_levelstext_lower = " (device warn/crit below %s/%s %s)" % (render_temp(
341 dev_warn_lower, output_unit), render_temp(dev_crit_lower,
342 output_unit), temp_unitsym[output_unit])
344 # Output only levels that are relevant when computing the state
345 if dlh == "usr":
346 infotext += usr_levelstext + usr_levelstext_lower
348 elif dlh == "dev":
349 infotext += dev_levelstext + dev_levelstext_lower
351 elif dlh in ("best", "worst"):
352 infotext += usr_levelstext + usr_levelstext_lower + dev_levelstext + dev_levelstext_lower
354 elif dlh == "devdefault":
355 infotext += dev_levelstext + dev_levelstext_lower
356 if not dev_levels:
357 infotext += usr_levelstext
358 if not dev_levels_lower:
359 infotext += usr_levelstext_lower
361 elif dlh == "usrdefault":
362 infotext += usr_levelstext + usr_levelstext_lower
363 if not usr_levels:
364 infotext += dev_levelstext
365 if not usr_levels_lower:
366 infotext += dev_levelstext_lower
368 # all checks specify a unique_name but when multiple sensors are handled through
369 # check_temperature_list, trend is only calculated for the average and then the individual
370 # calls to check_temperate receive no unique_name
371 # "trend_compute" in params tells us if there if there is configuration for trend computation
372 # when activating trend computation through the website, "period" is always set together with
373 # the trend_compute dictionary. But a check may want to specify default levels for trends
374 # without activating them. In this case they can leave period unset to deactivate the
375 # feature.
376 if unique_name and\
377 "trend_compute" in params\
378 and "period" in params["trend_compute"]:
379 trend_status, trend_infotext =\
380 check_temperature_trend(temp, params["trend_compute"], output_unit,
381 crit, crit_lower, unique_name)
382 status = max(status, trend_status)
383 if trend_infotext:
384 infotext += ", " + trend_infotext
386 return status, infotext, perfdata
389 # Wraps around check_temperature to check a list of sensors.
390 # sensorlist is a list of tuples:
391 # (subitem, temp, kwargs) or (subitem, temp)
392 # where subitem is a string (sensor-id)
393 # temp is a string, float or int temperature value
394 # and kwargs a dict of keyword arguments for check_temperature
397 def check_temperature_list(sensorlist, params, unique_name):
399 if isinstance(params, tuple):
400 params = {"levels": params}
401 elif params is None:
402 params = {}
404 output_unit = params.get("output_unit", "c")
406 def worststate(a, b):
407 if a != 3 and b != 3:
408 return max(a, b)
409 elif a != 2 and b != 2:
410 return 3
411 return 2
413 if sensorlist == []:
414 return
416 sensor_count = len(sensorlist)
417 tempsum = 0
418 tempmax = sensorlist[0][1]
419 tempmin = sensorlist[0][1]
420 status = 0
421 detailtext = ""
422 for entry in sensorlist:
424 if len(entry) == 2:
425 sub_item, temp = entry
426 kwargs = {}
427 else:
428 sub_item, temp, kwargs = entry
429 if not isinstance(temp, (float, int)):
430 temp = float(temp)
432 tempsum += temp
433 tempmax = max(tempmax, temp)
434 tempmin = min(tempmin, temp)
435 sub_status, sub_infotext, _sub_perfdata = check_temperature(temp, params, None, **kwargs)
436 status = worststate(status, sub_status)
437 if status != 0:
438 detailtext += (sub_item + ": " + sub_infotext + state_markers[sub_status] + ", ")
439 if detailtext:
440 detailtext = " " + detailtext[:-2] # Drop trailing ", ", add space to join with summary
442 unitsym = temp_unitsym[output_unit]
443 tempavg = tempsum / float(sensor_count)
444 summarytext = "%d Sensors; Highest: %s %s, Average: %s %s, Lowest: %s %s" % (
445 sensor_count, render_temp(tempmax, output_unit), unitsym, render_temp(
446 tempavg, output_unit), unitsym, render_temp(tempmin, output_unit), unitsym)
447 infotext = summarytext + detailtext
448 perfdata = [("temp", tempmax)]
450 if "trend_compute" in params and\
451 "period" in params["trend_compute"]:
452 usr_warn, usr_crit = params.get("levels") or (None, None)
453 usr_warn_lower, usr_crit_lower = params.get("levels_lower") or (None, None)
455 # no support for dev_unit or dev_levels in check_temperature_list so
456 # this ignores the device level handling set in params
457 _warn, crit, _warn_lower, crit_lower =\
458 check_temperature_determine_levels("usr", usr_warn, usr_crit,
459 usr_warn_lower, usr_crit_lower,
460 None, None,
461 None, None, "c")
464 trend_status, trend_infotext =\
465 check_temperature_trend(tempavg, params["trend_compute"], output_unit,
466 crit, crit_lower, unique_name)
467 status = max(status, trend_status)
468 if trend_infotext:
469 infotext += ", " + trend_infotext
471 return status, infotext, perfdata