2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
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):
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):
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):
48 return celsius_to_fahrenheit(tempc
, relative
)
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
])
60 return fahrenheit_to_celsius(reading
, relative
)
66 return reading
- 273.15
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):
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
,
88 # min that deals correctly with None
90 return min(a
, b
) or a
or b
92 # Ignore device's own levels
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
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.
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
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
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
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
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
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
)
149 combiner
.status
= status
151 if "infotext" in dir(combiner
):
152 combiner
.infotext
+= ", " + infotext
154 combiner
.infotext
= infotext
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
,
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"]
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
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"]]
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
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
209 minutes_left
= (diff_to_limit
/ rate_avg
) / 60.0
211 minutes_left
= float("inf")
213 def format_minutes(minutes
):
214 if minutes
> 60: # hours
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
:
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
,
248 dev_levels_lower
=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
:
254 elif crit_lower
is not None and temp
< crit_lower
:
256 elif warn
is not None and temp
>= warn
:
258 elif warn_lower
is not None and temp
< warn_lower
:
264 # Convert legacy tuple params into new dict
265 if params
is None or params
== (None, None):
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
,
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
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:
305 status
= min(status
, dev_status
)
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
320 usr_levelstext_lower
= ""
322 dev_levelstext_lower
= ""
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
])
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
])
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
])
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
346 infotext
+= usr_levelstext
+ usr_levelstext_lower
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
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
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
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
)
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
}
404 output_unit
= params
.get("output_unit", "c")
406 def worststate(a
, b
):
407 if a
!= 3 and b
!= 3:
409 elif a
!= 2 and b
!= 2:
416 sensor_count
= len(sensorlist
)
418 tempmax
= sensorlist
[0][1]
419 tempmin
= sensorlist
[0][1]
422 for entry
in sensorlist
:
425 sub_item
, temp
= entry
428 sub_item
, temp
, kwargs
= entry
429 if not isinstance(temp
, (float, int)):
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
)
438 detailtext
+= (sub_item
+ ": " + sub_infotext
+ state_markers
[sub_status
] + ", ")
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
,
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
)
469 infotext
+= ", " + trend_infotext
471 return status
, infotext
, perfdata