diff --git a/docs/api_boards.rst b/docs/api_boards.rst index cec9e7e..49919fb 100644 --- a/docs/api_boards.rst +++ b/docs/api_boards.rst @@ -127,6 +127,20 @@ Energenie :inherited-members: :members: +StatusZero +========== + +.. autoclass:: StatusZero + :inherited-members: + :members: + +StatusBoard +=========== + +.. autoclass:: StatusBoard + :inherited-members: + :members: + SnowPi ====== @@ -168,4 +182,3 @@ CompositeDevice .. autoclass:: CompositeDevice(\*args, _order=None, \*\*kwargs) :members: - diff --git a/docs/images/device_hierarchy.dot b/docs/images/device_hierarchy.dot index a794678..c3b6717 100644 --- a/docs/images/device_hierarchy.dot +++ b/docs/images/device_hierarchy.dot @@ -90,11 +90,13 @@ digraph classes { ButtonBoard->HoldMixin; PiLiter->LEDBoard; PiLiterBarGraph->LEDBarGraph; + StatusZero->LEDBoard; TrafficLights->LEDBoard; SnowPi->LEDBoard; PiTraffic->TrafficLights; PiStop->TrafficLights; TrafficLightsBuzzer->CompositeOutputDevice; + StatusBoard->CompositeOutputDevice; FishDish->TrafficLightsBuzzer; TrafficHat->TrafficLightsBuzzer; Robot->CompositeDevice; @@ -115,4 +117,3 @@ digraph classes { PingServer->InternalDevice; CPUTemperature->InternalDevice; } - diff --git a/docs/images/device_hierarchy.pdf b/docs/images/device_hierarchy.pdf index 5aecea9..c61e943 100644 Binary files a/docs/images/device_hierarchy.pdf and b/docs/images/device_hierarchy.pdf differ diff --git a/docs/images/device_hierarchy.png b/docs/images/device_hierarchy.png index 42e1073..28e47f3 100644 Binary files a/docs/images/device_hierarchy.png and b/docs/images/device_hierarchy.png differ diff --git a/docs/images/device_hierarchy.svg b/docs/images/device_hierarchy.svg index d5d5b09..35097c4 100644 --- a/docs/images/device_hierarchy.svg +++ b/docs/images/device_hierarchy.svg @@ -4,235 +4,235 @@ - - + + classes - + ValuesMixin - -ValuesMixin + +ValuesMixin SourceMixin - -SourceMixin + +SourceMixin SharedMixin - -SharedMixin + +SharedMixin EventsMixin - -EventsMixin + +EventsMixin HoldMixin - -HoldMixin + +HoldMixin Device - -Device + +Device Device->ValuesMixin - - + + GPIODevice - -GPIODevice + +GPIODevice GPIODevice->Device - - + + SmoothedInputDevice - -SmoothedInputDevice + +SmoothedInputDevice SmoothedInputDevice->EventsMixin - - + + InputDevice - -InputDevice + +InputDevice SmoothedInputDevice->InputDevice - - + + AnalogInputDevice - -AnalogInputDevice + +AnalogInputDevice SPIDevice - -SPIDevice + +SPIDevice AnalogInputDevice->SPIDevice - - + + MCP3xxx - -MCP3xxx + +MCP3xxx MCP3xxx->AnalogInputDevice - - + + MCP33xx - -MCP33xx + +MCP33xx MCP33xx->MCP3xxx - - + + CompositeDevice - -CompositeDevice + +CompositeDevice CompositeDevice->Device - - + + CompositeOutputDevice - -CompositeOutputDevice + +CompositeOutputDevice CompositeOutputDevice->SourceMixin - - + + CompositeOutputDevice->CompositeDevice - - + + LEDCollection - -LEDCollection + +LEDCollection LEDCollection->CompositeOutputDevice - - + + InternalDevice - -InternalDevice + +InternalDevice -InternalDevice->EventsMixin - - +InternalDevice->EventsMixin + + -InternalDevice->Device - - +InternalDevice->Device + + InputDevice->GPIODevice - - + + DigitalInputDevice - -DigitalInputDevice + +DigitalInputDevice DigitalInputDevice->EventsMixin - - + + DigitalInputDevice->InputDevice - - + + Button - -Button + +Button Button->HoldMixin - - + + Button->DigitalInputDevice - - + + MotionSensor - -MotionSensor + +MotionSensor MotionSensor->SmoothedInputDevice - - + + LightSensor - -LightSensor + +LightSensor LightSensor->SmoothedInputDevice - - + + LineSensor - -LineSensor + +LineSensor LineSensor->SmoothedInputDevice - - + + DistanceSensor - -DistanceSensor + +DistanceSensor DistanceSensor->SmoothedInputDevice - - + + OutputDevice @@ -241,488 +241,508 @@ OutputDevice->SourceMixin - - + + OutputDevice->GPIODevice - - + + DigitalOutputDevice - -DigitalOutputDevice + +DigitalOutputDevice DigitalOutputDevice->OutputDevice - - + + LED - -LED + +LED LED->DigitalOutputDevice - - + + Buzzer - -Buzzer + +Buzzer Buzzer->DigitalOutputDevice - - + + PWMOutputDevice - -PWMOutputDevice + +PWMOutputDevice PWMOutputDevice->OutputDevice - - + + PWMLED - -PWMLED + +PWMLED PWMLED->PWMOutputDevice - - + + RGBLED - -RGBLED + +RGBLED RGBLED->SourceMixin - - + + RGBLED->Device - - + + SPIDevice->Device - - + + MCP30xx - -MCP30xx + +MCP30xx MCP30xx->MCP3xxx - - + + MCP32xx - -MCP32xx + +MCP32xx MCP32xx->MCP3xxx - - + + MCP3xx2 - -MCP3xx2 + +MCP3xx2 MCP3xx2->MCP3xxx - - + + MCP3001 - -MCP3001 + +MCP3001 MCP3001->MCP30xx - - + + MCP3002 - -MCP3002 + +MCP3002 MCP3002->MCP30xx - - + + MCP3002->MCP3xx2 - - + + MCP3004 - -MCP3004 + +MCP3004 MCP3004->MCP30xx - - + + MCP3008 - -MCP3008 + +MCP3008 MCP3008->MCP30xx - - + + MCP3201 - -MCP3201 + +MCP3201 MCP3201->MCP32xx - - + + MCP3202 - -MCP3202 + +MCP3202 MCP3202->MCP32xx - - + + MCP3202->MCP3xx2 - - + + MCP3204 - -MCP3204 + +MCP3204 MCP3204->MCP32xx - - + + MCP3208 - -MCP3208 + +MCP3208 MCP3208->MCP32xx - - + + MCP3301 - -MCP3301 + +MCP3301 MCP3301->MCP33xx - - + + MCP3302 - -MCP3302 + +MCP3302 MCP3302->MCP33xx - - + + MCP3304 - -MCP3304 + +MCP3304 MCP3304->MCP33xx - - + + LEDBoard - -LEDBoard + +LEDBoard LEDBoard->LEDCollection - - + + LEDBarGraph - -LEDBarGraph + +LEDBarGraph LEDBarGraph->LEDCollection - - + + LedBorg - -LedBorg + +LedBorg LedBorg->RGBLED - - + + ButtonBoard - -ButtonBoard + +ButtonBoard ButtonBoard->HoldMixin - - + + ButtonBoard->CompositeDevice - - + + PiLiter - -PiLiter + +PiLiter PiLiter->LEDBoard - - + + PiLiterBarGraph - -PiLiterBarGraph + +PiLiterBarGraph PiLiterBarGraph->LEDBarGraph - - + + + + +StatusZero + +StatusZero + + +StatusZero->LEDBoard + + -TrafficLights - -TrafficLights +TrafficLights + +TrafficLights -TrafficLights->LEDBoard - - +TrafficLights->LEDBoard + + -SnowPi - -SnowPi +SnowPi + +SnowPi -SnowPi->LEDBoard - - +SnowPi->LEDBoard + + -PiTraffic - -PiTraffic +PiTraffic + +PiTraffic -PiTraffic->TrafficLights - - +PiTraffic->TrafficLights + + -PiStop - -PiStop +PiStop + +PiStop -PiStop->TrafficLights - - +PiStop->TrafficLights + + -TrafficLightsBuzzer - -TrafficLightsBuzzer +TrafficLightsBuzzer + +TrafficLightsBuzzer -TrafficLightsBuzzer->CompositeOutputDevice - - +TrafficLightsBuzzer->CompositeOutputDevice + + + + +StatusBoard + +StatusBoard + + +StatusBoard->CompositeOutputDevice + + -FishDish - -FishDish +FishDish + +FishDish -FishDish->TrafficLightsBuzzer - - +FishDish->TrafficLightsBuzzer + + -TrafficHat - -TrafficHat +TrafficHat + +TrafficHat -TrafficHat->TrafficLightsBuzzer - - +TrafficHat->TrafficLightsBuzzer + + -Robot - -Robot +Robot + +Robot -Robot->SourceMixin - - +Robot->SourceMixin + + -Robot->CompositeDevice - - +Robot->CompositeDevice + + -Energenie - -Energenie +Energenie + +Energenie -Energenie->SourceMixin - - +Energenie->SourceMixin + + -Energenie->Device - - +Energenie->Device + + -RyanteckRobot - -RyanteckRobot +RyanteckRobot + +RyanteckRobot -RyanteckRobot->Robot - - +RyanteckRobot->Robot + + -CamJamKitRobot - -CamJamKitRobot +CamJamKitRobot + +CamJamKitRobot -CamJamKitRobot->Robot - - +CamJamKitRobot->Robot + + -Motor - -Motor +Motor + +Motor -Motor->SourceMixin - - +Motor->SourceMixin + + -Motor->CompositeDevice - - +Motor->CompositeDevice + + -Servo - -Servo +Servo + +Servo -Servo->SourceMixin - - +Servo->SourceMixin + + -Servo->CompositeDevice - - +Servo->CompositeDevice + + -AngularServo - -AngularServo +AngularServo + +AngularServo -AngularServo->Servo - - +AngularServo->Servo + + -TimeOfDay - -TimeOfDay +TimeOfDay + +TimeOfDay -TimeOfDay->InternalDevice - - +TimeOfDay->InternalDevice + + -PingServer - -PingServer +PingServer + +PingServer -PingServer->InternalDevice - - +PingServer->InternalDevice + + -CPUTemperature - -CPUTemperature +CPUTemperature + +CPUTemperature -CPUTemperature->InternalDevice - - +CPUTemperature->InternalDevice + + diff --git a/gpiozero/__init__.py b/gpiozero/__init__.py index 31e8dc3..d0f2476 100644 --- a/gpiozero/__init__.py +++ b/gpiozero/__init__.py @@ -80,6 +80,8 @@ from .boards import ( TrafficLights, PiTraffic, PiStop, + StatusZero, + StatusBoard, SnowPi, TrafficLightsBuzzer, FishDish, diff --git a/gpiozero/boards.py b/gpiozero/boards.py index 7809fea..1a74136 100644 --- a/gpiozero/boards.py +++ b/gpiozero/boards.py @@ -12,7 +12,7 @@ except ImportError: from time import sleep from itertools import repeat, cycle, chain from threading import Lock -from collections import OrderedDict +from collections import OrderedDict, Counter from .exc import ( DeviceClosed, @@ -795,6 +795,102 @@ class PiStop(TrafficLights): pwm=pwm, initial_value=initial_value) +class StatusZero(LEDBoard): + """ + Extends :class:`LEDBoard` for The Pi Hut's `STATUS Zero`_: a Pi Zero sized + add-on board with three sets of red/green LEDs to provide a status + indicator. + + The following example designates the first strip the label "wifi" and the + second "raining", and turns them green and red respectfully:: + + from gpiozero import StatusZero + + status = StatusZero('wifi', 'raining') + status.wifi.green.on() + status.raining.red.on() + + :param str \*labels: + Specify the names of the labels you wish to designate the strips to. + You can list up to three labels. If no labels are given, three strips + will be initialised with names 'one', 'two', and 'three'. If some, but + not all strips are given labels, any remaining strips will not be + initialised. + + .. _STATUS Zero: https://thepihut.com/statuszero + """ + default_labels = ('one', 'two', 'three') + + def __init__(self, *labels, **kwargs): + pins = ( + (17, 4), + (22, 27), + (9, 10), + ) + if len(labels) == 0: + labels = self.default_labels + elif len(labels) > len(pins): + raise ValueError("StatusZero doesn't support more than three labels") + dup, count = Counter(labels).most_common(1)[0] + if count > 1: + raise ValueError("Duplicate label %s" % dup) + super(StatusZero, self).__init__(_order=labels, **{ + label: LEDBoard(red=red, green=green, _order=('red', 'green'), **kwargs) + for (green, red), label in zip(pins, labels) + }) + + +class StatusBoard(CompositeOutputDevice): + """ + Extends :class:`CompositeOutputDevice` for The Pi Hut's `STATUS`_ board: a + HAT sized add-on board with five sets of red/green LEDs and buttons to + provide a status indicator with additional input. + + The following example designates the first strip the label "wifi" and the + second "raining", turns the wifi green and then activates the button to + toggle its lights when pressed:: + + from gpiozero import StatusBoard + + status = StatusBoard('wifi', 'raining') + status.wifi.lights.green.on() + status.wifi.button.when_pressed = status.wifi.lights.toggle + + :param str \*labels: + Specify the names of the labels you wish to designate the strips to. + You can list up to three labels. If no labels are given, three strips + will be initialised with names 'one' to 'five'. If some, but not all + strips are given labels, any remaining strips will not be initialised. + + .. _STATUS: https://thepihut.com/status + """ + default_labels = ('one', 'two', 'three', 'four', 'five') + + def __init__(self, *labels, **kwargs): + pins = ( + (17, 4, 14), + (22, 27, 19), + (9, 10, 15), + (5, 11, 26), + (13, 6, 18), + ) + if len(labels) == 0: + labels = self.default_labels + elif len(labels) > len(pins): + raise ValueError("StatusBoard doesn't support more than five labels") + dup, count = Counter(labels).most_common(1)[0] + if count > 1: + raise ValueError("Duplicate label %s" % dup) + super(StatusBoard, self).__init__(_order=labels, **{ + label: CompositeOutputDevice( + button=Button(button), + lights=LEDBoard( + red=red, green=green, _order=('red', 'green'), **kwargs + ), _order=('button', 'lights')) + for (green, red, button), label in zip(pins, labels) + }) + + class SnowPi(LEDBoard): """ Extends :class:`LEDBoard` for the `Ryanteck SnowPi`_ board. @@ -1184,4 +1280,3 @@ class Energenie(SourceMixin, Device): def off(self): self.value = False - diff --git a/gpiozero/devices.py b/gpiozero/devices.py index e993b0d..346c677 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -315,23 +315,28 @@ class CompositeDevice(Device): self._named = frozendict({}) self._namedtuple = None self._order = kwargs.pop('_order', None) - if self._order is None: - self._order = sorted(kwargs.keys()) - else: - for missing_name in set(kwargs.keys()) - set(self._order): - raise CompositeDeviceBadOrder('%s missing from _order' % missing_name) - self._order = tuple(self._order) - super(CompositeDevice, self).__init__() - for name in set(self._order) & set(dir(self)): - raise CompositeDeviceBadName('%s is a reserved name' % name) + try: + if self._order is None: + self._order = sorted(kwargs.keys()) + else: + for missing_name in set(kwargs.keys()) - set(self._order): + raise CompositeDeviceBadOrder('%s missing from _order' % missing_name) + self._order = tuple(self._order) + for name in set(self._order) & set(dir(self)): + raise CompositeDeviceBadName('%s is a reserved name' % name) + for dev in chain(args, kwargs.values()): + if not isinstance(dev, Device): + raise CompositeDeviceBadDevice("%s doesn't inherit from Device" % dev) + self._named = frozendict(kwargs) + self._namedtuple = namedtuple('%sValue' % self.__class__.__name__, chain( + ('device_%d' % i for i in range(len(args))), self._order)) + except: + for dev in chain(args, kwargs.values()): + if isinstance(dev, Device): + dev.close() + raise self._all = args + tuple(kwargs[v] for v in self._order) - for dev in self._all: - if not isinstance(dev, Device): - raise CompositeDeviceBadDevice("%s doesn't inherit from Device" % dev) - self._named = frozendict(kwargs) - self._namedtuple = namedtuple('%sValue' % self.__class__.__name__, chain( - (str(i) for i in range(len(args))), self._order), - rename=True) + super(CompositeDevice, self).__init__() def __getattr__(self, name): # if _named doesn't exist yet, pretend it's an empty dict @@ -377,6 +382,7 @@ class CompositeDevice(Device): if self._all: for device in self._all: device.close() + self._all = () @property def closed(self): diff --git a/tests/test_boards.py b/tests/test_boards.py index 1ba4879..983bfee 100644 --- a/tests/test_boards.py +++ b/tests/test_boards.py @@ -30,6 +30,8 @@ def setup_function(function): 'test_led_board_fade_background', 'test_led_bar_graph_pwm_value', 'test_led_bar_graph_pwm_initial_value', + 'test_statusboard_kwargs', + 'test_statuszero_kwargs', ) else MockPin def teardown_function(function): @@ -778,3 +780,113 @@ def test_energenie(): pins[5].assert_states_and_times([(0.0, False), (0.1, True), (0.25, False)]) device1.close() assert repr(device1) == '' + +def test_statuszero_init(): + with StatusZero() as sz: + assert sz.namedtuple._fields == ('one', 'two', 'three') + with StatusZero('a') as sz: + assert sz.namedtuple._fields == ('a',) + with StatusZero('a', 'b') as sz: + assert sz.namedtuple._fields == ('a', 'b') + with StatusZero('a', 'b', 'c') as sz: + assert sz.namedtuple._fields == ('a', 'b', 'c') + with pytest.raises(ValueError): + StatusZero('a', 'b', 'c', 'd') + with pytest.raises(ValueError): + StatusZero('0') + with pytest.raises(ValueError): + StatusZero('foo', 'hello world') + with pytest.raises(ValueError): + StatusZero('foo', 'foo') + +def test_statuszero(): + with StatusZero() as sz: + assert isinstance(sz.one, LEDBoard) + assert isinstance(sz.two, LEDBoard) + assert isinstance(sz.three, LEDBoard) + assert isinstance(sz.one.red, LED) + assert isinstance(sz.one.green, LED) + assert sz.value == ((False, False), (False, False), (False, False)) + sz.on() + assert sz.value == ((True, True), (True, True), (True, True)) + sz.one.green.off() + assert sz.one.value == (True, False) + +def test_statuszero_kwargs(): + with StatusZero(pwm=True, initial_value=True) as sz: + assert isinstance(sz.one, LEDBoard) + assert isinstance(sz.two, LEDBoard) + assert isinstance(sz.three, LEDBoard) + assert isinstance(sz.one.red, PWMLED) + assert isinstance(sz.one.green, PWMLED) + assert sz.value == ((1, 1), (1, 1), (1, 1)) + sz.off() + assert sz.value == ((0, 0), (0, 0), (0, 0)) + +def test_statuszero_named(): + with StatusZero('a') as sz: + assert isinstance(sz.a, LEDBoard) + assert isinstance(sz.a.red, LED) + with pytest.raises(AttributeError): + sz.one + +def test_statusboard_init(): + with StatusBoard() as sb: + assert sb.namedtuple._fields == ('one', 'two', 'three', 'four', 'five') + with StatusBoard('a') as sb: + assert sb.namedtuple._fields == ('a',) + with StatusBoard('a', 'b') as sb: + assert sb.namedtuple._fields == ('a', 'b',) + with StatusBoard('a', 'b', 'c', 'd', 'e') as sb: + assert sb.namedtuple._fields == ('a', 'b', 'c', 'd', 'e') + with pytest.raises(ValueError): + StatusBoard('a', 'b', 'c', 'd', 'e', 'f') + with pytest.raises(ValueError): + StatusBoard('0') + with pytest.raises(ValueError): + StatusBoard('foo', 'hello world') + with pytest.raises(ValueError): + StatusBoard('foo', 'foo') + +def test_statusboard(): + with StatusBoard() as sb: + assert isinstance(sb.one, CompositeOutputDevice) + assert isinstance(sb.two, CompositeOutputDevice) + assert isinstance(sb.five, CompositeOutputDevice) + assert isinstance(sb.one.button, Button) + assert isinstance(sb.one.lights, LEDBoard) + assert isinstance(sb.one.lights.red, LED) + assert isinstance(sb.one.lights.green, LED) + assert sb.value == ((False, (False, False)), (False, (False, False)), + (False, (False, False)), (False, (False, False)), + (False, (False, False))) + sb.on() + assert sb.value == ((False, (True, True)), (False, (True, True)), + (False, (True, True)), (False, (True, True)), + (False, (True, True))) + sb.one.lights.green.off() + assert sb.one.value == (False, (True, False)) + +def test_statusboard_kwargs(): + with StatusBoard(pwm=True, initial_value=True) as sb: + assert isinstance(sb.one, CompositeOutputDevice) + assert isinstance(sb.two, CompositeOutputDevice) + assert isinstance(sb.five, CompositeOutputDevice) + assert isinstance(sb.one.button, Button) + assert isinstance(sb.one.lights, LEDBoard) + assert isinstance(sb.one.lights.red, PWMLED) + assert isinstance(sb.one.lights.green, PWMLED) + assert sb.value == ((False, (1, 1)), (False, (1, 1)), (False, (1, 1)), + (False, (1, 1)), (False, (1, 1))) + sb.off() + assert sb.value == ((False, (0, 0)), (False, (0, 0)), (False, (0, 0)), + (False, (0, 0)), (False, (0, 0))) + +def test_statusboard_named(): + with StatusBoard('a') as sb: + assert isinstance(sb.a, CompositeOutputDevice) + assert isinstance(sb.a.button, Button) + assert isinstance(sb.a.lights, LEDBoard) + assert isinstance(sb.a.lights.red, LED) + with pytest.raises(AttributeError): + sb.one diff --git a/tests/test_devices.py b/tests/test_devices.py index de5fcd0..7293741 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -91,7 +91,7 @@ def test_composite_device_sequence(): assert len(device) == 2 assert device[0].pin.number == 4 assert device[1].pin.number == 5 - assert device.namedtuple._fields == ('_0', '_1') + assert device.namedtuple._fields == ('device_0', 'device_1') def test_composite_device_values(): with CompositeDevice(