Skip to content

Commit

Permalink
Merge pull request #941 from gpiozero/sample-internal
Browse files Browse the repository at this point in the history
Implement polling for internal devices
  • Loading branch information
waveform80 committed Mar 14, 2021
2 parents c403999 + 03446dc commit 9506ac1
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 126 deletions.
3 changes: 3 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ images/output_device_hierarchy.dot: $(PY_SOURCES)
images/input_device_hierarchy.dot: $(PY_SOURCES)
./images/class_graph -i InputDevice -o EventsMixin -o HoldMixin > $@

images/internal_device_hierarchy.dot: $(PY_SOURCES)
./images/class_graph -i InternalDevice -o EventsMixin > $@

%.svg: %.mscgen
mscgen -T svg -o $@ $<

Expand Down
79 changes: 65 additions & 14 deletions docs/api_internal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,56 @@ GPIO Zero also provides several "internal" devices which represent facilities
provided by the operating system itself. These can be used to react to things
like the time of day, or whether a server is available on the network.

.. warning::
These devices provide an API similar to and compatible with GPIO devices so that
internal device events can trigger changes to GPIO output devices the way input
devices can. In the same way a :class:`~gpiozero.Button` object is *active* when
it's pressed, and can be used to trigger other devices when its state changes,
a :class:`TimeOfDay` object is *active* during a particular time period.

These devices are experimental and their API is not yet considered stable.
We welcome any comments from testers, especially regarding new "internal
devices" that you'd find useful!
Consider the following code in which a :class:`~gpiozero.Button` object is used
to control an :class:`~gpiozero.LED` object::

from gpiozero import LED, Button
from signal import pause

led = LED(2)
btn = Button(3)

btn.when_pressed = led.on
btn.when_released = led.off

pause()

Now consider the following example in which a :class:`TimeOfDay` object is used
to control an :class:`~gpiozero.LED` using the same method::

from gpiozero import LED, TimeOfDay
from datetime import time
from signal import pause

led = LED(2)
tod = TimeOfDay(time(9), time(10))

tod.when_activated = led.on
tod.when_deactivated = led.off

pause()

Here, rather than the LED being controlled by the press of a button, it's
controlled by the time. When the time reaches 09:00AM, the LED comes on, and at
10:00AM it goes off.

Like the :class:`~gpiozero.Button` object, internal devices like the
:class:`TimeOfDay` object has :attr:`~TimeOfDay.value`,
:attr:`~TimeOfDay.values`, :attr:`~TimeOfDay.is_active`,
:attr:`~TimeOfDay.when_activated` and :attr:`~TimeOfDay.when_deactivated`
attributes, so alternative methods using the other paradigms would also work.

.. note::
Note that although the constructor parameter ``pin_factory`` is available
for internal devices, and is required to be valid, the pin factory chosen
will not make any practical difference. Reading a remote Pi's CPU
temperature, for example, is not currently possible.


Regular Classes
Expand All @@ -57,36 +102,36 @@ named after. All classes in this section are concrete (not abstract).
TimeOfDay
---------

.. autoclass:: TimeOfDay(start_time, end_time, *, utc=True, pin_factory=None)
:members: start_time, end_time, utc, value
.. autoclass:: TimeOfDay(start_time, end_time, *, utc=True, event_delay=10.0, pin_factory=None)
:members: start_time, end_time, utc, value, is_active, when_activated, when_deactivated


PingServer
----------

.. autoclass:: PingServer(host, *, pin_factory=None)
:members: host, value
.. autoclass:: PingServer(host, *, event_delay=10.0, pin_factory=None)
:members: host, value, is_active, when_activated, when_deactivated


CPUTemperature
--------------

.. autoclass:: CPUTemperature(sensor_file='/sys/class/thermal/thermal_zone0/temp', *, min_temp=0.0, max_temp=100.0, threshold=80.0, pin_factory=None)
:members: temperature, value, is_active
.. autoclass:: CPUTemperature(sensor_file='/sys/class/thermal/thermal_zone0/temp', *, min_temp=0.0, max_temp=100.0, threshold=80.0, event_delay=5.0, pin_factory=None)
:members: temperature, value, is_active, when_activated, when_deactivated


LoadAverage
-----------

.. autoclass:: LoadAverage(load_average_file='/proc/loadavg', *, min_load_average=0.0, max_load_average=1.0, threshold=0.8, minutes=5, pin_factory=None)
:members: load_average, value, is_active
.. autoclass:: LoadAverage(load_average_file='/proc/loadavg', *, min_load_average=0.0, max_load_average=1.0, threshold=0.8, minutes=5, event_delay=10.0, pin_factory=None)
:members: load_average, value, is_active, when_activated, when_deactivated


DiskUsage
---------

.. autoclass:: DiskUsage(filesystem='/', *, threshold=90.0, pin_factory=None)
:members: usage, value, is_active
.. autoclass:: DiskUsage(filesystem='/', *, threshold=90.0, event_delay=30.0, pin_factory=None)
:members: usage, value, is_active, when_activated, when_deactivated


Base Classes
Expand All @@ -103,6 +148,12 @@ The following sections document these base classes for advanced users that wish
to construct classes for their own devices.


PolledInternalDevice
--------------------

.. autoclass:: PolledInternalDevice(*, event_delay=1.0, pin_factory=None)


InternalDevice
--------------

Expand Down
4 changes: 2 additions & 2 deletions docs/examples/internet_status_indicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

google = PingServer('google.com')

green.source = google
green.source_delay = 60
google.when_activated = green.on
google.when_deactivated = green.off
red.source = negated(green)

pause()
4 changes: 2 additions & 2 deletions docs/examples/timed_heat_lamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
lamp = Energenie(1)
daytime = TimeOfDay(time(8), time(20))

lamp.source = daytime
lamp.source_delay = 60
daytime.when_activated = lamp.on
daytime.when_deactivated = lamp.off

pause()
2 changes: 2 additions & 0 deletions gpiozero/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
SourceMixin,
ValuesMixin,
EventsMixin,
event,
HoldMixin,
)
from .input_devices import (
Expand Down Expand Up @@ -145,6 +146,7 @@
)
from .internal_devices import (
InternalDevice,
PolledInternalDevice,
PingServer,
CPUTemperature,
LoadAverage,
Expand Down
15 changes: 8 additions & 7 deletions gpiozero/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,13 @@ def __del__(self):

def close(self):
"""
Shut down the device and release all associated resources. This method
can be called on an already closed device without raising an exception.
Shut down the device and release all associated resources (such as GPIO
pins).
This method is primarily intended for interactive use at the command
line. It disables the device and releases its pin(s) for use by another
device.
This method is idempotent (can be called on an already closed device
without any side-effects). It is primarily intended for interactive use
at the command line. It disables the device and releases its pin(s) for
use by another device.
You can attempt to do this simply by deleting an object, but unless
you've cleaned up all references to the object this may not work (even
Expand Down Expand Up @@ -196,8 +197,8 @@ def close(self):
"""
# This is a placeholder which is simply here to ensure close() can be
# safely called from subclasses without worrying whether super-classes
# have it (which in turn is useful in conjunction with the SourceMixin
# class).
# have it (which in turn is useful in conjunction with the mixin
# classes).
pass

@property
Expand Down
53 changes: 22 additions & 31 deletions gpiozero/input_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
from .exc import InputDeviceError, DeviceClosed, DistanceSensorNoEcho, \
PinInvalidState, PWMSoftwareFallback
from .devices import GPIODevice, CompositeDevice
from .mixins import GPIOQueue, EventsMixin, HoldMixin
from .mixins import GPIOQueue, EventsMixin, HoldMixin, event
try:
from .pins.pigpio import PiGPIOFactory
except ImportError:
Expand Down Expand Up @@ -1216,8 +1216,7 @@ def _change_state(self, ticks, edge):
-self._max_steps if self._wrap else self._max_steps
)
self._rotate_cw_event.set()
if self._when_rotated_cw:
self._when_rotated_cw()
self._fire_rotated_cw()
self._rotate_cw_event.clear()
elif new_state == '-1':
self._steps = (
Expand All @@ -1226,15 +1225,13 @@ def _change_state(self, ticks, edge):
self._max_steps if self._wrap else -self._max_steps
)
self._rotate_ccw_event.set()
if self._when_rotated_ccw:
self._when_rotated_ccw()
self._fire_rotated_ccw()
self._rotate_ccw_event.clear()
else:
self._state = new_state
return
self._rotate_event.set()
if self._when_rotated:
self._when_rotated()
self._fire_rotated()
self._rotate_event.clear()
self._fire_events(ticks, self.is_active)
self._state = 'idle'
Expand Down Expand Up @@ -1278,8 +1275,7 @@ def wait_for_rotate_counter_clockwise(self, timeout=None):
"""
return self._rotate_ccw_event.wait(timeout)

@property
def when_rotated(self):
when_rotated = event(
"""
The function to be run when the encoder is rotated in either direction.
Expand All @@ -1290,15 +1286,9 @@ def when_rotated(self):
as that parameter.
Set this property to :data:`None` (the default) to disable the event.
"""
return self._when_rotated

@when_rotated.setter
def when_rotated(self, value):
self._when_rotated = self._wrap_callback(value)
""")

@property
def when_rotated_clockwise(self):
when_rotated_clockwise = event(
"""
The function to be run when the encoder is rotated clockwise.
Expand All @@ -1309,15 +1299,9 @@ def when_rotated_clockwise(self):
as that parameter.
Set this property to :data:`None` (the default) to disable the event.
"""
return self._when_rotated_cw

@when_rotated_clockwise.setter
def when_rotated_clockwise(self, value):
self._when_rotated_cw = self._wrap_callback(value)
""")

@property
def when_rotated_counter_clockwise(self):
when_rotated_counter_clockwise = event(
"""
The function to be run when the encoder is rotated counter-clockwise.
Expand All @@ -1328,12 +1312,7 @@ def when_rotated_counter_clockwise(self):
as that parameter.
Set this property to :data:`None` (the default) to disable the event.
"""
return self._when_rotated_ccw

@when_rotated_counter_clockwise.setter
def when_rotated_counter_clockwise(self, value):
self._when_rotated_ccw = self._wrap_callback(value)
""")

@property
def steps(self):
Expand All @@ -1353,6 +1332,18 @@ def steps(self):
"""
return self._steps

def _fire_rotated(self):
if self.when_rotated:
self.when_rotated()

def _fire_rotated_cw(self):
if self.when_rotated_clockwise:
self.when_rotated_clockwise()

def _fire_rotated_ccw(self):
if self.when_rotated_counter_clockwise:
self.when_rotated_counter_clockwise()

@steps.setter
def steps(self, value):
value = int(value)
Expand Down

0 comments on commit 9506ac1

Please sign in to comment.