Coverage for /usr/lib/python3/dist-packages/gpiozero/internal_devices.py: 28%
214 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-02-10 12:38 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-02-10 12:38 +0000
1# vim: set fileencoding=utf-8:
2#
3# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
4#
5# Copyright (c) 2016-2021 Dave Jones <dave@waveform.org.uk>
6# Copyright (c) 2017-2021 Ben Nuttall <ben@bennuttall.com>
7# Copyright (c) 2019 Jeevan M R <14.jeevan@gmail.com>
8# Copyright (c) 2019 Andrew Scheller <github@loowis.durge.org>
9#
10# SPDX-License-Identifier: BSD-3-Clause
12from __future__ import (
13 unicode_literals,
14 print_function,
15 absolute_import,
16 division,
17)
18str = type('')
21import os
22import io
23import subprocess
24from datetime import datetime, time
25import warnings
27from .devices import Device
28from .mixins import EventsMixin, event
29from .threads import GPIOThread
30from .exc import ThresholdOutOfRange, DeviceClosed
33class InternalDevice(EventsMixin, Device):
34 """
35 Extends :class:`Device` to provide a basis for devices which have no
36 specific hardware representation. These are effectively pseudo-devices and
37 usually represent operating system services like the internal clock, file
38 systems or network facilities.
39 """
40 def __init__(self, pin_factory=None):
41 self._closed = False
42 super(InternalDevice, self).__init__(pin_factory=pin_factory)
44 def close(self):
45 self._closed = True
46 super(InternalDevice, self).close()
48 @property
49 def closed(self):
50 return self._closed
52 def __repr__(self):
53 try:
54 self._check_open()
55 return "<gpiozero.%s object>" % self.__class__.__name__
56 except DeviceClosed:
57 return "<gpiozero.%s object closed>" % self.__class__.__name__
60class PolledInternalDevice(InternalDevice):
61 """
62 Extends :class:`InternalDevice` to provide a background thread to poll
63 internal devices that lack any other mechanism to inform the instance of
64 changes.
65 """
66 def __init__(self, event_delay=1.0, pin_factory=None):
67 self._event_thread = None
68 self._event_delay = event_delay
69 super(PolledInternalDevice, self).__init__(pin_factory=pin_factory)
71 def close(self):
72 try:
73 self._start_stop_events(False)
74 except AttributeError:
75 pass # pragma: no cover
76 super(PolledInternalDevice, self).close()
78 @property
79 def event_delay(self):
80 """
81 The delay between sampling the device's value for the purposes of
82 firing events.
84 Note that this only applies to events assigned to attributes like
85 :attr:`~EventsMixin.when_activated` and
86 :attr:`~EventsMixin.when_deactivated`. When using the
87 :attr:`~SourceMixin.source` and :attr:`~ValuesMixin.values` properties,
88 the sampling rate is controlled by the
89 :attr:`~SourceMixin.source_delay` property.
90 """
91 return self._event_delay
93 @event_delay.setter
94 def event_delay(self, value):
95 self._event_delay = float(value)
97 def wait_for_active(self, timeout=None):
98 self._start_stop_events(True)
99 try:
100 return super(PolledInternalDevice, self).wait_for_active(timeout)
101 finally:
102 self._start_stop_events(
103 self.when_activated or self.when_deactivated)
105 def wait_for_inactive(self, timeout=None):
106 self._start_stop_events(True)
107 try:
108 return super(PolledInternalDevice, self).wait_for_inactive(timeout)
109 finally:
110 self._start_stop_events(
111 self.when_activated or self.when_deactivated)
113 def _watch_value(self):
114 while not self._event_thread.stopping.wait(self._event_delay):
115 self._fire_events(self.pin_factory.ticks(), self.is_active)
117 def _start_stop_events(self, enabled):
118 if self._event_thread and not enabled:
119 self._event_thread.stop()
120 self._event_thread = None
121 elif not self._event_thread and enabled:
122 self._event_thread = GPIOThread(self._watch_value)
123 self._event_thread.start()
126class PingServer(PolledInternalDevice):
127 """
128 Extends :class:`PolledInternalDevice` to provide a device which is active
129 when a *host* (domain name or IP address) can be pinged.
131 The following example lights an LED while ``google.com`` is reachable::
133 from gpiozero import PingServer, LED
134 from signal import pause
136 google = PingServer('google.com')
137 led = LED(4)
139 google.when_activated = led.on
140 google.when_deactivated = led.off
142 pause()
144 :param str host:
145 The hostname or IP address to attempt to ping.
147 :type event_delay: float
148 :param event_delay:
149 The number of seconds between pings (defaults to 10 seconds).
151 :type pin_factory: Factory or None
152 :param pin_factory:
153 See :doc:`api_pins` for more information (this is an advanced feature
154 which most users can ignore).
155 """
156 def __init__(self, host, event_delay=10.0, pin_factory=None):
157 self._host = host
158 super(PingServer, self).__init__(
159 event_delay=event_delay, pin_factory=pin_factory)
160 self._fire_events(self.pin_factory.ticks(), self.is_active)
162 def __repr__(self):
163 try:
164 self._check_open()
165 return '<gpiozero.PingServer object host="%s">' % self.host
166 except DeviceClosed:
167 return super(PingServer, self).__repr__()
169 @property
170 def host(self):
171 """
172 The hostname or IP address to test whenever :attr:`value` is queried.
173 """
174 return self._host
176 @property
177 def value(self):
178 """
179 Returns :data:`1` if the host returned a single ping, and :data:`0`
180 otherwise.
181 """
182 # XXX This is doing a DNS lookup every time it's queried; should we
183 # call gethostbyname in the constructor and ping that instead (good
184 # for consistency, but what if the user *expects* the host to change
185 # address?)
186 with io.open(os.devnull, 'wb') as devnull:
187 try:
188 subprocess.check_call(
189 ['ping', '-c1', self.host],
190 stdout=devnull, stderr=devnull)
191 except subprocess.CalledProcessError:
192 return 0
193 else:
194 return 1
196 when_activated = event(
197 """
198 The function to run when the device changes state from inactive
199 (host unresponsive) to active (host responsive).
201 This can be set to a function which accepts no (mandatory)
202 parameters, or a Python function which accepts a single mandatory
203 parameter (with as many optional parameters as you like). If the
204 function accepts a single mandatory parameter, the device that
205 activated it will be passed as that parameter.
207 Set this property to ``None`` (the default) to disable the event.
208 """)
210 when_deactivated = event(
211 """
212 The function to run when the device changes state from inactive
213 (host responsive) to active (host unresponsive).
215 This can be set to a function which accepts no (mandatory)
216 parameters, or a Python function which accepts a single mandatory
217 parameter (with as many optional parameters as you like). If the
218 function accepts a single mandatory parameter, the device that
219 activated it will be passed as that parameter.
221 Set this property to ``None`` (the default) to disable the event.
222 """)
225class CPUTemperature(PolledInternalDevice):
226 """
227 Extends :class:`PolledInternalDevice` to provide a device which is active
228 when the CPU temperature exceeds the *threshold* value.
230 The following example plots the CPU's temperature on an LED bar graph::
232 from gpiozero import LEDBarGraph, CPUTemperature
233 from signal import pause
235 # Use minimums and maximums that are closer to "normal" usage so the
236 # bar graph is a bit more "lively"
237 cpu = CPUTemperature(min_temp=50, max_temp=90)
239 print('Initial temperature: {}C'.format(cpu.temperature))
241 graph = LEDBarGraph(5, 6, 13, 19, 25, pwm=True)
242 graph.source = cpu
244 pause()
246 :param str sensor_file:
247 The file from which to read the temperature. This defaults to the
248 sysfs file :file:`/sys/class/thermal/thermal_zone0/temp`. Whatever
249 file is specified is expected to contain a single line containing the
250 temperature in milli-degrees celsius.
252 :param float min_temp:
253 The temperature at which :attr:`value` will read 0.0. This defaults to
254 0.0.
256 :param float max_temp:
257 The temperature at which :attr:`value` will read 1.0. This defaults to
258 100.0.
260 :param float threshold:
261 The temperature above which the device will be considered "active".
262 (see :attr:`is_active`). This defaults to 80.0.
264 :type event_delay: float
265 :param event_delay:
266 The number of seconds between file reads (defaults to 5 seconds).
268 :type pin_factory: Factory or None
269 :param pin_factory:
270 See :doc:`api_pins` for more information (this is an advanced feature
271 which most users can ignore).
272 """
273 def __init__(self, sensor_file='/sys/class/thermal/thermal_zone0/temp',
274 min_temp=0.0, max_temp=100.0, threshold=80.0, event_delay=5.0,
275 pin_factory=None):
276 self.sensor_file = sensor_file
277 super(CPUTemperature, self).__init__(
278 event_delay=event_delay, pin_factory=pin_factory)
279 try:
280 if min_temp >= max_temp:
281 raise ValueError('max_temp must be greater than min_temp')
282 self.min_temp = min_temp
283 self.max_temp = max_temp
284 if not min_temp <= threshold <= max_temp:
285 warnings.warn(ThresholdOutOfRange(
286 'threshold is outside of the range (min_temp, max_temp)'))
287 self.threshold = threshold
288 self._fire_events(self.pin_factory.ticks(), self.is_active)
289 except:
290 self.close()
291 raise
293 def __repr__(self):
294 try:
295 self._check_open()
296 return '<gpiozero.CPUTemperature object temperature=%.2f>' % self.temperature
297 except DeviceClosed:
298 return super(CPUTemperature, self).__repr__()
300 @property
301 def temperature(self):
302 """
303 Returns the current CPU temperature in degrees celsius.
304 """
305 with io.open(self.sensor_file, 'r') as f:
306 return float(f.read().strip()) / 1000
308 @property
309 def value(self):
310 """
311 Returns the current CPU temperature as a value between 0.0
312 (representing the *min_temp* value) and 1.0 (representing the
313 *max_temp* value). These default to 0.0 and 100.0 respectively, hence
314 :attr:`value` is :attr:`temperature` divided by 100 by default.
315 """
316 temp_range = self.max_temp - self.min_temp
317 return (self.temperature - self.min_temp) / temp_range
319 @property
320 def is_active(self):
321 """
322 Returns :data:`True` when the CPU :attr:`temperature` exceeds the
323 *threshold*.
324 """
325 return self.temperature > self.threshold
327 when_activated = event(
328 """
329 The function to run when the device changes state from inactive to
330 active (temperature reaches *threshold*).
332 This can be set to a function which accepts no (mandatory)
333 parameters, or a Python function which accepts a single mandatory
334 parameter (with as many optional parameters as you like). If the
335 function accepts a single mandatory parameter, the device that
336 activated it will be passed as that parameter.
338 Set this property to ``None`` (the default) to disable the event.
339 """)
341 when_deactivated = event(
342 """
343 The function to run when the device changes state from active to
344 inactive (temperature drops below *threshold*).
346 This can be set to a function which accepts no (mandatory)
347 parameters, or a Python function which accepts a single mandatory
348 parameter (with as many optional parameters as you like). If the
349 function accepts a single mandatory parameter, the device that
350 activated it will be passed as that parameter.
352 Set this property to ``None`` (the default) to disable the event.
353 """)
356class LoadAverage(PolledInternalDevice):
357 """
358 Extends :class:`PolledInternalDevice` to provide a device which is active
359 when the CPU load average exceeds the *threshold* value.
361 The following example plots the load average on an LED bar graph::
363 from gpiozero import LEDBarGraph, LoadAverage
364 from signal import pause
366 la = LoadAverage(min_load_average=0, max_load_average=2)
367 graph = LEDBarGraph(5, 6, 13, 19, 25, pwm=True)
369 graph.source = la
371 pause()
373 :param str load_average_file:
374 The file from which to read the load average. This defaults to the
375 proc file :file:`/proc/loadavg`. Whatever file is specified is expected
376 to contain three space-separated load averages at the beginning of the
377 file, representing 1 minute, 5 minute and 15 minute averages
378 respectively.
380 :param float min_load_average:
381 The load average at which :attr:`value` will read 0.0. This defaults to
382 0.0.
384 :param float max_load_average:
385 The load average at which :attr:`value` will read 1.0. This defaults to
386 1.0.
388 :param float threshold:
389 The load average above which the device will be considered "active".
390 (see :attr:`is_active`). This defaults to 0.8.
392 :param int minutes:
393 The number of minutes over which to average the load. Must be 1, 5 or
394 15. This defaults to 5.
396 :type event_delay: float
397 :param event_delay:
398 The number of seconds between file reads (defaults to 10 seconds).
400 :type pin_factory: Factory or None
401 :param pin_factory:
402 See :doc:`api_pins` for more information (this is an advanced feature
403 which most users can ignore).
404 """
405 def __init__(self, load_average_file='/proc/loadavg', min_load_average=0.0,
406 max_load_average=1.0, threshold=0.8, minutes=5, event_delay=10.0,
407 pin_factory=None):
408 if min_load_average >= max_load_average:
409 raise ValueError(
410 'max_load_average must be greater than min_load_average')
411 self.load_average_file = load_average_file
412 self.min_load_average = min_load_average
413 self.max_load_average = max_load_average
414 if not min_load_average <= threshold <= max_load_average:
415 warnings.warn(ThresholdOutOfRange(
416 'threshold is outside of the range (min_load_average, '
417 'max_load_average)'))
418 self.threshold = threshold
419 if minutes not in (1, 5, 15):
420 raise ValueError('minutes must be 1, 5 or 15')
421 self._load_average_file_column = {
422 1: 0,
423 5: 1,
424 15: 2,
425 }[minutes]
426 super(LoadAverage, self).__init__(
427 event_delay=event_delay, pin_factory=pin_factory)
428 self._fire_events(self.pin_factory.ticks(), None)
430 def __repr__(self):
431 try:
432 self._check_open()
433 return '<gpiozero.LoadAverage object load average=%.2f>' % self.load_average
434 except DeviceClosed:
435 return super(LoadAverage, self).__repr__()
437 @property
438 def load_average(self):
439 """
440 Returns the current load average.
441 """
442 with io.open(self.load_average_file, 'r') as f:
443 print(repr(f))
444 file_columns = f.read().strip().split()
445 return float(file_columns[self._load_average_file_column])
447 @property
448 def value(self):
449 """
450 Returns the current load average as a value between 0.0 (representing
451 the *min_load_average* value) and 1.0 (representing the
452 *max_load_average* value). These default to 0.0 and 1.0 respectively.
453 """
454 load_average_range = self.max_load_average - self.min_load_average
455 return (self.load_average - self.min_load_average) / load_average_range
457 @property
458 def is_active(self):
459 """
460 Returns :data:`True` when the :attr:`load_average` exceeds the
461 *threshold*.
462 """
463 return self.load_average > self.threshold
465 when_activated = event(
466 """
467 The function to run when the device changes state from inactive to
468 active (load average reaches *threshold*).
470 This can be set to a function which accepts no (mandatory)
471 parameters, or a Python function which accepts a single mandatory
472 parameter (with as many optional parameters as you like). If the
473 function accepts a single mandatory parameter, the device that
474 activated it will be passed as that parameter.
476 Set this property to ``None`` (the default) to disable the event.
477 """)
479 when_deactivated = event(
480 """
481 The function to run when the device changes state from active to
482 inactive (load average drops below *threshold*).
484 This can be set to a function which accepts no (mandatory)
485 parameters, or a Python function which accepts a single mandatory
486 parameter (with as many optional parameters as you like). If the
487 function accepts a single mandatory parameter, the device that
488 activated it will be passed as that parameter.
490 Set this property to ``None`` (the default) to disable the event.
491 """)
494class TimeOfDay(PolledInternalDevice):
495 """
496 Extends :class:`PolledInternalDevice` to provide a device which is active
497 when the computer's clock indicates that the current time is between
498 *start_time* and *end_time* (inclusive) which are :class:`~datetime.time`
499 instances.
501 The following example turns on a lamp attached to an :class:`Energenie`
502 plug between 07:00AM and 08:00AM::
504 from gpiozero import TimeOfDay, Energenie
505 from datetime import time
506 from signal import pause
508 lamp = Energenie(1)
509 morning = TimeOfDay(time(7), time(8))
511 morning.when_activated = lamp.on
512 morning.when_deactivated = lamp.off
514 pause()
516 Note that *start_time* may be greater than *end_time*, indicating a time
517 period which crosses midnight.
519 :param ~datetime.time start_time:
520 The time from which the device will be considered active.
522 :param ~datetime.time end_time:
523 The time after which the device will be considered inactive.
525 :param bool utc:
526 If :data:`True` (the default), a naive UTC time will be used for the
527 comparison rather than a local time-zone reading.
529 :type event_delay: float
530 :param event_delay:
531 The number of seconds between file reads (defaults to 10 seconds).
533 :type pin_factory: Factory or None
534 :param pin_factory:
535 See :doc:`api_pins` for more information (this is an advanced feature
536 which most users can ignore).
537 """
538 def __init__(self, start_time, end_time, utc=True, event_delay=5.0,
539 pin_factory=None):
540 self._start_time = None
541 self._end_time = None
542 self._utc = True
543 super(TimeOfDay, self).__init__(
544 event_delay=event_delay, pin_factory=pin_factory)
545 try:
546 self._start_time = self._validate_time(start_time)
547 self._end_time = self._validate_time(end_time)
548 if self.start_time == self.end_time:
549 raise ValueError('end_time cannot equal start_time')
550 self._utc = utc
551 self._fire_events(self.pin_factory.ticks(), self.is_active)
552 except:
553 self.close()
554 raise
556 def __repr__(self):
557 try:
558 self._check_open()
559 return '<gpiozero.TimeOfDay object active between %s and %s %s>' % (
560 self.start_time, self.end_time, ('local', 'UTC')[self.utc])
561 except DeviceClosed:
562 return super(TimeOfDay, self).__repr__()
564 def _validate_time(self, value):
565 if isinstance(value, datetime):
566 value = value.time()
567 if not isinstance(value, time):
568 raise ValueError(
569 'start_time and end_time must be a datetime, or time instance')
570 return value
572 @property
573 def start_time(self):
574 """
575 The time of day after which the device will be considered active.
576 """
577 return self._start_time
579 @property
580 def end_time(self):
581 """
582 The time of day after which the device will be considered inactive.
583 """
584 return self._end_time
586 @property
587 def utc(self):
588 """
589 If :data:`True`, use a naive UTC time reading for comparison instead of
590 a local timezone reading.
591 """
592 return self._utc
594 @property
595 def value(self):
596 """
597 Returns :data:`1` when the system clock reads between :attr:`start_time`
598 and :attr:`end_time`, and :data:`0` otherwise. If :attr:`start_time` is
599 greater than :attr:`end_time` (indicating a period that crosses
600 midnight), then this returns :data:`1` when the current time is
601 greater than :attr:`start_time` or less than :attr:`end_time`.
602 """
603 now = datetime.utcnow().time() if self.utc else datetime.now().time()
604 if self.start_time < self.end_time:
605 return int(self.start_time <= now <= self.end_time)
606 else:
607 return int(not self.end_time < now < self.start_time)
609 when_activated = event(
610 """
611 The function to run when the device changes state from inactive to
612 active (time reaches *start_time*).
614 This can be set to a function which accepts no (mandatory)
615 parameters, or a Python function which accepts a single mandatory
616 parameter (with as many optional parameters as you like). If the
617 function accepts a single mandatory parameter, the device that
618 activated it will be passed as that parameter.
620 Set this property to ``None`` (the default) to disable the event.
621 """)
623 when_deactivated = event(
624 """
625 The function to run when the device changes state from active to
626 inactive (time reaches *end_time*).
628 This can be set to a function which accepts no (mandatory)
629 parameters, or a Python function which accepts a single mandatory
630 parameter (with as many optional parameters as you like). If the
631 function accepts a single mandatory parameter, the device that
632 activated it will be passed as that parameter.
634 Set this property to ``None`` (the default) to disable the event.
635 """)
638class DiskUsage(PolledInternalDevice):
639 """
640 Extends :class:`PolledInternalDevice` to provide a device which is active
641 when the disk space used exceeds the *threshold* value.
643 The following example plots the disk usage on an LED bar graph::
645 from gpiozero import LEDBarGraph, DiskUsage
646 from signal import pause
648 disk = DiskUsage()
650 print('Current disk usage: {}%'.format(disk.usage))
652 graph = LEDBarGraph(5, 6, 13, 19, 25, pwm=True)
653 graph.source = disk
655 pause()
657 :param str filesystem:
658 A path within the filesystem for which the disk usage needs to be
659 computed. This defaults to :file:`/`, which is the root filesystem.
661 :param float threshold:
662 The disk usage percentage above which the device will be considered
663 "active" (see :attr:`is_active`). This defaults to 90.0.
665 :type event_delay: float
666 :param event_delay:
667 The number of seconds between file reads (defaults to 30 seconds).
669 :type pin_factory: Factory or None
670 :param pin_factory:
671 See :doc:`api_pins` for more information (this is an advanced feature
672 which most users can ignore).
673 """
674 def __init__(self, filesystem='/', threshold=90.0, event_delay=30.0,
675 pin_factory=None):
676 super(DiskUsage, self).__init__(
677 event_delay=event_delay, pin_factory=pin_factory)
678 os.statvfs(filesystem)
679 if not 0 <= threshold <= 100:
680 warnings.warn(ThresholdOutOfRange(
681 'threshold is outside of the range (0, 100)'))
682 self.filesystem = filesystem
683 self.threshold = threshold
684 self._fire_events(self.pin_factory.ticks(), None)
686 def __repr__(self):
687 try:
688 self._check_open()
689 return '<gpiozero.DiskUsage object usage=%.2f>' % self.usage
690 except DeviceClosed:
691 return super(DiskUsage, self).__repr__()
693 @property
694 def usage(self):
695 """
696 Returns the current disk usage in percentage.
697 """
698 return self.value * 100
700 @property
701 def value(self):
702 """
703 Returns the current disk usage as a value between 0.0 and 1.0 by
704 dividing :attr:`usage` by 100.
705 """
706 # This slightly convoluted calculation is equivalent to df's "Use%";
707 # it calculates the percentage of FS usage as a proportion of the
708 # space available to *non-root users*. Technically this means it can
709 # exceed 100% (when FS is filled to the point that only root can write
710 # to it), hence the clamp.
711 vfs = os.statvfs(self.filesystem)
712 used = vfs.f_blocks - vfs.f_bfree
713 total = used + vfs.f_bavail
714 return min(1.0, used / total)
716 @property
717 def is_active(self):
718 """
719 Returns :data:`True` when the disk :attr:`usage` exceeds the
720 *threshold*.
721 """
722 return self.usage > self.threshold
724 when_activated = event(
725 """
726 The function to run when the device changes state from inactive to
727 active (disk usage reaches *threshold*).
729 This can be set to a function which accepts no (mandatory)
730 parameters, or a Python function which accepts a single mandatory
731 parameter (with as many optional parameters as you like). If the
732 function accepts a single mandatory parameter, the device that
733 activated it will be passed as that parameter.
735 Set this property to ``None`` (the default) to disable the event.
736 """)
738 when_deactivated = event(
739 """
740 The function to run when the device changes state from active to
741 inactive (disk usage drops below *threshold*).
743 This can be set to a function which accepts no (mandatory)
744 parameters, or a Python function which accepts a single mandatory
745 parameter (with as many optional parameters as you like). If the
746 function accepts a single mandatory parameter, the device that
747 activated it will be passed as that parameter.
749 Set this property to ``None`` (the default) to disable the event.
750 """)