-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebthing_dimmable_LED_strip.py
More file actions
1576 lines (1320 loc) · 70.1 KB
/
webthing_dimmable_LED_strip.py
File metadata and controls
1576 lines (1320 loc) · 70.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# check for TODO
"""
Module: webthing_dimmable_LED_strip.py
Purpose:
Webthing to control a non-addressable LED strip.
A webthing is a set of objects that present a standardised web interface to
improve inter-operability of "Internet of Things" devices.
The present webthing manages the lighting of LED strips that are controlled
through power.
===============================================================================
Author: Alain Culos
programming-electronics@asoundmove.net
Bio: I love programming (pro), I love data (pro), I enjoy electronics as
a hobby.
I have worked on small and large software projects in various
industries: Defense (distributed systems, real-time, several
seconds), Air Traffic Control (mainframes, real-time, sub-second),
FinTech (software as a service, real-time, 10-100 milliseconds),
Avionics testing (embedded, milliseconds) and Banking (various
credit decision platforms, back office).
Now building up my python credentials (learning, practice,
sharing and giving back to the community).
Module history:
2020-08-xx: proof of concept: electronics, driving the electronics with
python on a Pi4
2020-09-18: requirements specification and project documentation (the text
below)
2020-09-23: start development of the webthing - getting the basics of
webthing to work
2020-09-24: webthing works with all POC functionality and a bit more:
Switching on and off or setting channels cleverly switches the
relay on and off appropriately (i.e. depending whether all
channels are 0).
Also checking that values are only set for channels that belong
to the control.
2020-10-04: V1.0
webthing works with all basic functionality and loading
calibration curves from files.
Standard properties implemented:
on
'@type': 'OnOffProperty',
brightness
'@type': 'BrightnessProperty',
colour
'@type': 'ColorProperty',
Non-standard properties implemented:
channel_brightness
'@type': 'ChannelBrightnessProperty',
channel_curve
'@type': 'ChannelCurveProperty',
Other key developments:
load_calibration (and the Compute-LED-calibration.py that
feeds it, based on the output of multiple runs, one per
channel, of PWM_and_MOSFET_calibration.py)
Some manual fine tuning of the calibration happens in
Compute-LED-calibration.py
We could have integrated this step in here, but I did not
want to have to run webthing with pandas and I did not want
to introduce an extra delay at start-up.
2020-10-07: Improved inline documentation
2020-10-16: Completed wiring documentation
2020-11-23: After much testing on a gateway add-on, it turns out that using
the id field to store useful data is lost on clients of the
gateway.
New solution: implement a "location" property.
2021-02-12: Adding BME280 for temperature, humidity and pressure monitoring
Hardware addition, early software development of the sensor
functions, my beginnings at using asyncio.
TODO, problems to solve:
1/ SW: think about how to terminate the execution of a pattern mid-way
2/ SW: exception handling
3/ SW: test automation
4/ SW: Consider https://github.com/hidaris/thingtalk for asyncio programming
===============================================================================
Future:
This is a hobby project to control LED lights in my home. There may be new
functions to develop to make the lights easier or more fun to use.
As this is a hobby project, it will remain perfect in its imperfection:
sufficiently good to do the job rather efficiently. Of course it is not
production grade, it does not need to be.
Copyright:
Alain P.M. Culos 2020-2021, all rights reserved.
Licence:
You shall not use this code as part of a commercial activity without
reaching an explicit written commercial agreement with the author for
exclusive or non exclusive commercial use rights. Commercial activity
includes any activity that facilitates directly or indirectly another
commercial activity even if this software is not used in a commercial
product per se.
This includes private education, R&D even if indirectly linked to profit...
You may use the present code and text in part or whole free of charge for
any non-commercial activity.
This includes state sponsored education, volunteer run organisations, not
for profit organisations, and of course hobbyists, open source projects.
You must acknowledge the author, include and respect the terms of this
licence.
Disclaimer:
Use at your own risk.
The author makes no claim as to suitability for purpose. Any consequence is
beyond my control and entirely your responsability. Before you use this you
should have the relevant knowledge to handle electronics and electrical
power. The author is not an electrician or an electronics specialist so he
cannot provide advice. You should also undesrtand the relationships between
software and hardware as overheating electronics can cause fires and bare
wires can cause electrocution.
The purpose of sharing the present file is for self learning and exchange
with like-minded people (curious, tinkerer, maker, DIYer).
===============================================================================
Low level functions:
Raspberry Pi -> power switch to power supply to feed the LED strip(s)
:: ON, OFF
Raspberry Pi -> PWM to control the LED strip(s) intensity
:: SET <CH#> <level>
Raspberry Pi -> Raspberry Pi to control the LED strip(s) intensity
:: CONFIGURE <CH#> <translation>
PWM -> MOS FET to control the LED strip(s) intensity
Power supply -> MOS FET to feed the LED strip(s)
MOS FET -> LED strip(s) to feed and control the LED strip(s)
intensity
Weather -> Temperature, Humidity & Pressure sensor
Note:
ON Dimmable_LED_strip_webthing.OnOff
OFF Dimmable_LED_strip_webthing.OnOff
SET Dimmable_LED_strip_webthing.channel_brightness
CONFIGURE # done at initialisation of
"Dimmable_LED_strip_channels"
Webthing capabilities (what this module provides):
Switch on: ON if OFF, SET
Use last setting or predefined setting depending on
configuration
Switch off: OFF, SET to level 0
TODO: Remember pointer in current pattern (pause yield
loop?)
Configure curve: CONFIGURE
Set the translation curve for light intensity
(intended) to PWM ratio Note: on the MOS FET board each
channel behaves quite differently, at the same time,
the LED colours seem to also behave differently: for
both small variances in input can yield large
differences in light output but not at the same points
in the curve.
See "PWM_and_MOSFET_calibration.py" and
"Compute-LED-calibration.py" for details on how to
perform a semi-automatic calibration.
Bear in mind that potentially each MOSFET / LED strip
combination potentially requires different calibration
data.
We could imagine a self-calibration set-up with an ADC
on the MOS FET output, but that also poses the question
of calibration of the ADC channels at least relative to
one another. Plus since calibration is probably only
required once, the ADC set-up should be a removable
"plug-in" (literally).
Set brightness: ON if level > 0, SET, OFF if level = 0
Dimmable_LED_strip_webthing.brightness
Set colour: ON if level > 0, SET, OFF if level = 0
Dimmable_LED_strip_webthing.channel_brightness
Dimmable_LED_strip_webthing.colour
Image pattern: ON if OFF, SET, OFF if last level = 0, if not looping
TODO - not yet implemented
Array pattern: ON if OFF, SET, OFF if last level = 0, if not looping
TODO - not yet implemented
Function pattern: ON if OFF, SET, OFF if last level = 0, if not looping
TODO - not yet implemented
Physical diagram:
+---------+
| Weather |
+---------+
^
+----+ +-----+ | +-----+ +---------+ +------------+
| 5V |--->| rPi |-+->| PWM |-4 ch->| MOS FET |-> 4 channels ->| LED strips |
+----+ +-----+ +-----+ +---------+ +------------+
| ^
v +---------------+ |
+-------+ +-------+ | Transformer | |
| Mains |->| Relay |->| Mains --> LED |-+
+-------+ +-------+ | strip voltage |
+---------------+
Notes:
Depending on the chosen MOS FET board, it is possible to accommodate a wide
variety of voltages.
Wiring:
Mains - Relay
Mains: Live, Brown -> Relay: COM (terminal block, middle)
Mains - 5V transformer
Mains: Live, Brown -> 5V: ACL
Mains: Neutral, Blue-> 5V: ACN
Mains - 12V transformer
Mains: Neutral, Blue-> 12V: ACN
5V - Pi
5V: +5V -> Pi: Pin02, 5V (or USB power cable, red wire)
5V: GND, 0V -> Pi: Pin06, GND (or USB power cable, black wire)
Note: beware to only connect the 5V supply to one place on the Pi.
It can be via the PWR USB input or via a 5V pin, but not both.
5V - Relay
5V: +5V -> Relay: VCC (pin nearest green LED)
5V: GND, 0V -> Relay: GND (middle pin)
5V - PWM: I2C bus + 5V
5V: +5V -> PCA9685: Long side, Power block (2 screws), V+
(left, if at the top)
5V: GND, 0V -> PCA9685: Long side, Power block (2 screws), GND
Relay - 12V transformer
Relay: NO TB, left -> 12V: ACL
12V - MOS FET
12V: DC - -> MOS FET: DC -
12V: DC + -> MOS FET: DC +
Pi - Relay: GPIO + resistor
Pi: GPIO23, Pin16 -> one side of 10k Resistor
10kR: other side -> Relay: IN (pin nearest red LED)
Pi - PWM: I2C bus + 5V
Pi: +3.3V, Pin01 -> PCA9685: VCC, Short side, Pin5
(2nd from bottom, if on the left)
Pi: SDA, Pin03 -> PCA9685: SDA, Short side, Pin4
Pi: SCL, Pin05 -> PCA9685: SCL, Short side, Pin3
Pi: GND, Pin09 -> PCA9685: GND, Short side, Pin1 (top)
Note: it is safe to power PWMs that control a MOS FET from the
Raspberry Pi Zero 5V, but it would not be safe to do so to control
motors.
However to keep wring neater, and as the power supply is available in
the same vicinity as the Pi, I will power everything directly from the
power supply.
Pi - Weather: I2C bus
Pi: +3.3V, Pin01 -> BME280: VIN
Pi: SDA, Pin03 -> BME280: SDA
Pi: SCL, Pin05 -> BME280: SCL
Pi: GND, Pin09 -> BME280: GND
PWM - MOS FET
PWM: PWM 0 -> MOS FET: PWM 1
PWM: PWM 1 -> MOS FET: PWM 2
PWM: PWM 2 -> MOS FET: PWM 3
PWM: PWM 3 -> MOS FET: PWM 4
PWM: GND 0 -> MOS FET: GND 1
PWM: GND 1 -> MOS FET: GND 2
PWM: GND 2 -> MOS FET: GND 3
PWM: GND 3 -> MOS FET: GND 4
MOS FET - LED
MOS FET: OUT 1 - -> LED RGB: Red (Channel 0)
MOS FET: OUT 2 - -> LED RGB: Green (Channel 1)
MOS FET: OUT 3 - -> LED White: Black (Channel 2)
MOS FET: OUT 4 - -> LED RGB: Blue (Channel 3)
MOS FET: OUT 1 + -> LED RGB: Black
MOS FET: OUT 2 + -> LED RGB: Black
MOS FET: OUT 3 + -> LED White: Red
MOS FET: OUT 4 + -> LED RGB: Black
Implementation detail:
The Raspberry Pi communicates with the PWM board via the i2c bus.
The Raspberry Pi controls the relay via a GPIO (out of course) + 10 kOhms
resistor.
The PWM outputs each control a MOS FET input.
Implementation tested:
POC developped on Raspberry Pi4
Target implementation (all these can be changed to suit your needs):
Raspberry Pi0
Purpose: Where the logic happens: where the (python) "webthing"
program lives.
The reason for the Raspberry Pi0 as opposed to Arduino,
is the ease of development
The reasons for the Raspberry Pi0 as opposed to Pi2, 3
or 4 are cost and small footprint
The reason for Pi0 plain as opposed to wireless are the
potential instability of wireless
Network via Ethernet USB dongle as well as cost
Buy: https://thepihut.com/products/raspberry-pi-zero
Specification: https://www.raspberrypi.org/products/raspberry-pi-zero/
Power: https://www.circuits.dk/everything-about-raspberry-gpio/
Max 50mA on 3.3V pins
Max 16mA on any GPIO pin (not all at oncei: max 50mA total)
PSU-100mA on 5V supply (including USB)
GPIOs really available:
4 (7), 5 (29), 6 (31),
12 (32), 13 (33),
16 (36), 17 (11),
20 (38), 21 (40), 22 (15), 23 (16),
24 (18), 25 (22), 26 (37), 27 (13)
3V3 (1) (2) 5V
I2C SDA GPIO2 (3) (4) 5V
I2C SCL GPIO3 (5) (6) GND
GPIO4 (7) (8) GPIO14 Serial
GND (9) (10) GPIO15 Serial
GPIO17 (11) (12) GPIO18 HW PWM
GPIO27 (13) (14) GND
GPIO22 (15) (16) GPIO23
3V3 (17) (18) GPIO24
SPI GPIO10 (19) (20) GND
SPI GPIO9 (21) (22) GPIO25
SPI GPIO11 (23) (24) GPIO8 SPI
GND (25) (26) GPIO7 SPI
Reserved GPIO0 (27) (28) GPIO1 Reserved
GPIO5 (29) (30) GND
GPIO6 (31) (32) GPIO12
GPIO13 (33) (34) GND
HW PWM GPIO19 (35) (36) GPIO16
GPIO26 (37) (38) GPIO20
GND (39) (40) GPIO21
PWM
Purpose: The Raspberry Pi only has 2 hardware PWMs, one of which
cannot be used if the I2C bus is being used. Software
PWM is too unreliable.
To control 3 or more channels, an extra board is
required, with a key advantage that the external PWM
frees up Raspberry Pi resources.
This board offers 16 PWM channels via the I2C bus.
Buy: https://www.aliexpress.com/item/4000468996665.html
Specification: https://cdn-shop.adafruit.com/datasheets/PCA9685.pdf
Notes: 16 channels, of which we use 4, but we could use up to
62 of these boards as they can be configured with
soldering any of 6 hardware pins. That could command
992 channels!
To power this many PWMs, a beefier 5V supply might be
required, and if all channels drove power LEDs, many
and stronger transformers would be required to supply
the load.
How-to: https://learn.adafruit.com/16-channel-pwm-servo-driver/python-circuitpython
https://circuitpython.readthedocs.io/projects/pca9685/en/latest/api.html
Power: According to the datasheet for this board, the I2C bus
is capable of drawing 30mA, but it is not clear how
much it actually draws. Another source was quoting 30
micro Amps!
The outputs are powered by the 5V supply.
MOS FET F5305S
Purpose: Modulate (dim) and drive 12V power to the LED strips
Buy: https://www.aliexpress.com/item/33015020793.html
Specification: see above
Notes: 4 channels, add more boards to drive more channels.
How-to: Plug PWM outputs to board inputs,
Plug reference voltage,
Plug LED power wires to be driven.
Power: 5mA per input (driven by PWM 5V)
5A max output (20A with heat sinks)
Relay
Purpose: Switch off the 12V power supply when not required (save
residual current draw when lights are off)
Buy: https://www.aliexpress.com/item/32909882481.html
How-to: https://www.instructables.com/id/5V-Relay-Module-Mod-to-Work-With-Raspberry-Pi/
Notes: No need to go out to town on the mod, simply inserting
a 10kOhm resistor between the Pi GPIO and the "in" pin
of the relay works for me.
GPIO = 0, means COM connects to NC and disconnects from NO
GPIO = 1, means COM connects to NO and disconnects from NC
Weather sensor
Micro SD card
Buy: Not anywhere! Watch the quality, some cheap ones do not
work well for very long on the Raspberry Pi.
Specification: Class 10
How-to:
Part 1 - prepare the SD card - this requires a different
computer, instructions here for Linux
# Note: this prepares a headless system
apt install rpi-imager # OR
snap install rpi-imager
rpi-imager
# Select Raspberry OS 32 bit lite, select the correct
# SD card, write to it
# Remove and re-insert the SD card
# Replace the "*" below with the correct path
touch /media/*/boot/ssh
nano /media/*/rootfs/etc/dhcpcd.conf
# Set static address and routing (use your text editor
# of choice, personally I prefer vim but nano seems
# standard in any published instructions)
nano /media/*/rootfs/etc/hostname
umount boot; umount rootfs
ssh pi@<ip>
Part 2 - connect the Raspberry Pi zero
Insert micro SD card,
Connect micro USB Ethernet dongle & Ethernet cable (or
go wireless, but you are on your own),
Connect power
Note: if the green LED near the micro USB socket marked
"PWR IN" blinks 7 times, it means the SD card is
corrupt. This can happen after a reboot with a bad
card, if it happens twice with the same card, discard
it and use a different card.
Part 3 - configure the Raspberry Pi zero
passwd
# change default password to something personal
apt update
# update package database
apt upgrade
# upgrade packages already installed that need updating
apt install screen vim
# some useful tools
apt install python3-pip
# essential tool
raspi-configi
# Interfacing options, enable i2c, enable remote gpio,
# enable serial
mkdir -p Documents/GPIO-programming
cd !:2
python3 -m pip install RPi.GPIO
python3 -m pip install Adafruit-GPIO
# This may be unnecessary, could included i the
# following update
python3 -m pip install adafruit-circuitpython-adafruitio
python3 -m pip install adafruit-circuitpython-pca9685
python3 -m pip install adafruit-circuitpython-bme280
python3 -m pip install adafruit-circuitpython-ads1x15
# useful for calibration, if you have the ADS1115
python3 -m pip install webthing
Part 4 - develop/install/test/customise this program
Development:
A lot of help got from the following links (and
many others):
https://github.com/mozilla-iot/webthing-python/tree/master/webthing
https://github.com/WebThingsIO/webthing-python/blob/master/webthing/thing.py
https://github.com/WebThingsIO/webthing-python/blob/master/webthing/property.py
https://github.com/WebThingsIO/webthing-python/blob/master/webthing/value.py
https://github.com/WebThingsIO/webthing-python/blob/5b779b5d3e545c93d636a0f6fac1582512cba62d/example/multiple-things.py
https://iot.mozilla.org/wot/
Testing:
Although I did not document testing, I covered a
lot of test cases (unit, integration, user
stories).
Obviously I may have missed some cases, please
share your observations.
Part 5 - backup the SD card
shutdown -h 0
# Power off the Raspberry Pi Zero
# Remove the SD card and place in a different computer
# Take an image copy of this SD card so it can be
# easily restored in the event of a failure
# In linux (check which device the card comes under)
dd if=/dev/sdc of=/Your/Backup/Directory/BackupNameForYourSDCard.img
# I suggest the backup name should contain a date and
# the hostname or IP address of your Pi0
# Put the SD card in the Pi0
# Power-up the Pi
# If it does not work, you most likely have a low
# quality SD card
Part 6 - use the gateway
On a Raspberry Pi 4, install the arm deb package from
(note the Gateway does not work on Pi0):
https://github.com/WebThingsIO/gateway/releases
Do this rather than the image if you want to retain
full control of your Raspberry Pi 4, for example
because you use it for other purposes.
The installation enables a service which gets started
at boot.
Ethernet USB dongle
Buy: https://www.aliexpress.com/item/32970621991.html
How-to: Plug & play - need to configure the network (static
address is probably best, makes it easier to ssh into
the right box)
Dupont line
Buy: https://www.aliexpress.com/item/4000192140351.html
Specification: Your requirements may vary
Notes: This is mostly useful for prototyping
Transformer / Power supply - 5V
Buy: https://www.aliexpress.com/item/4000094353564.html
Notes: Needs to cater for the Raspberry Pi, PWM board, MOS
FET, Relay
All well under 2A (in fact well under 1A), but let's
build in some space for growth, a 2A, 5V power supply
is inexpensive.
How-to: See wiring above - always wire with the power off.
Transformer / Power supply - 12V
Buy: https://www.aliexpress.com/item/4000094353564.html
Notes: Needs to cater for the LED strips.
In my scenario, this should have been about 3 or 4A,
but measuring the current consumption (with a
multimeter) shows that when all four PWMs are fully on,
with my two strips (one RGB, one White), the current
used is only about 2A.
Again, allowing for a bit of growth, let's take a 4A
power supply.
How-to: See wiring above - always wire with the power off.
RGB LED Strip
Buy: https://www.aliexpress.com/item/32809840774.html
Specification: RGB + 5050 120led IP21
How-to: See wiring above - always wire with the power off.
White LED Strip
Buy: https://www.amazon.co.uk/gp/product/B00C6SHZSE/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1
Description: Waterproof Cool White DC 12V 5M 3528 SMD 300 Leds LED
Strips Strip Light
How-to: See wiring above - always wire with the power off.
"""
# Using https://iot.mozilla.org/framework/ as a template
# Not sure why or how, but `division` is used by `webthing`
# Webthing enables exposing a standard interface for controlling Internet of
# Things objects.
from __future__ import division
from webthing import (Action, Event, Property, MultipleThings, Thing, Value,
WebThingServer)
import logging # First port of call for debugging
import time
import math
import asyncio
#import uuid # TODO: currently unused (came from the example webthing)
import RPi.GPIO as GPIO # To control the 12V relay
import board # To use the I2C bus
import busio # To use the I2C bus
# PWM - PCA9685
# extension board that can produce up to 16 HW PWMs 40 Hz to 1600 Hz.
from adafruit_pca9685 import PCA9685
import adafruit_bme280
import pickle
import numpy
#TODO: develop an auto-off timer function - which would trigger this event
"""
class AutoOffEvent(Event):
def __init__(self, thing, data):
Event.__init__(self, thing, 'auto-off', data=data)
"""
#TODO: develop functions to produce smooth intensity transitions when switchiing lights on and off, or changing levels
"""
class FadeAction(Action):
def __init__(self, thing, input_):
Action.__init__(self, uuid.uuid4().hex, thing, 'fade', input_=input_)
def perform_action(self):
time.sleep(self.input['duration'] / 1000)
self.thing.set_property('brightness', self.input['brightness'])
self.thing.add_event(OverheatedEvent(self.thing, 102))
"""
def scale(value, max_value, name = "value"):
"""
"""
v = 0 if value <= 0 else max_value if value >= 1 else int(value * max_value)
logging.debug(f"{name}={value:0.3f} x {max_value} -> scaled {name}={v}")
return v
class Dimmable_LED_strip_channels():
"""
This class manages the hardware interface: GPIOs and I2C communications for
control of the PWM channels.
The reason for separating this layer from the webthing is that we can
define muultiple instances of Dimmable_LED_strip_webthing that refer to
common channels.
This allows to have the best of both worlds: one can manage 4 channels as
one entity, or split them in 3+1 channels for example, and change between
the two at any time.
"""
def __init__(self, on_off_channel, i2c_bus, frequency, channels, channel_curves):
"""
on_off_channel # The GPIO output pin that controls the relay to
# the transformer
i2c_bus # The I2C bus object - passed as a parameter to
# avoid multiple declarations
frequency # The frequency at which we will operate the PWM
# and therefore the lights
# Note that frequency and channel_curves are
# inter-dependant.
channels # The PWM channels to use for all lights
channel_curves # These curves attempt to correct the non linearity
# between the PWM control value and the voltage
# (TODO: or power?) output.
# The curves are the result of a "manual"
# calibration process.
# See module PWM_and_MOSFET_calibration.py for
# details.
channels = { # A dictionary that associates channel names with
"channel name": # Can be anything, it is a label, this will be the
# key to colour data
channel_number, # Channel on the PWM board, this corresponds to a
# given pair of wires
...
}
channel_curves = { # A dictionary that associates channel names with
# curves.
"channel name": # Key to colour data, same same as in the
# `channels` parameter
{ # The curve: a dictionary that associates keys with
# values.
brightness: # The key is a number from 0 to 1 which is the
# intended brightness of the channel to which
# corresponds:
PWM_duty_cycle, # The value which is also a number from 0 to 1 but
# represents the PWM duty cycle that needs to be
# set to achieve the wanted brightness.
...
}
}
"""
self.on_off_channel = on_off_channel
self.channels = channels
self.channel_curves = channel_curves
# set-up communication with the PWM board
self.PWM_board = PCA9685(i2c_bus)
self.things = {}
self.all_things = []
self.value = {}
self.last_on_value = {}
self.default_value = {}
self.PWM_board.frequency = frequency
def register_thing_with_LED_strip_channels(self, thing):
"""
As this module provides for multiple ways to access LED channels, we
need to keep tabs as to which "webthing" object refers to which
channel, so that we are able to notify the relevant subscribers of
state changes.
"""
# First call: set-up the GPIO pin to be an output (it controls the
# relay to the transformer) as this pin is shared by all channels, it
# only makes sense to initialise it once. set it to OFF (no power)
if len(self.all_things) == 0:
GPIO.setup(self.on_off_channel, GPIO.OUT)
GPIO.output(self.on_off_channel, False)
# Register all "webthing"s that use any channel
if not thing in self.all_things:
self.all_things.append(thing)
logging.info(
'Dimmable_LED_strip_channels: '
f'len={len(self.all_things)}: {self.all_things}.'
)
# For each channel, register all "webthing"s that use this channel
for c in thing.channels:
if not c in self.things:
self.things[c] = [thing, ]
logging.info(
'Dimmable_LED_strip_channels: '
f'Setting "{thing.title}" to channel {c}.'
)
else:
self.things[c].append(thing)
logging.info(
'Dimmable_LED_strip_channels: '
f'Adding "{thing.title}" to channel {c}.'
)
# initialise channel current value
self.value[c] = 0.0
# initialise channel value when "webthing" was last ON
self.last_on_value[c] = 0.0
# initialise channel default value: if last_on are all 0, use default
self.default_value[c] = 0.2
logging.info(
'Dimmable_LED_strip_channels: '
f'Associated "{c}" channel & '
f'on/off pin {self.on_off_channel} '
f'with {", ".join((f"""{t.title}""" for t in self.things[c]))}.'
)
def OnOff(self, thing, value):
"""
Switch channel on (value is True) or off (value is False)
If all channels are at 0 (not just those of the current "webthing"
(thing)), then turn off the relay.
If at least one channel is non zero, then turn on the relay.
"""
logging.info(
'Dimmable_LED_strip_channels: '
f'{self.on_off_channel} to {"ON" if value else "OFF"}.'
)
if value:
# Switch LEDs ON
# Notify subscribers of all webthings that use any channel of the
# change to the "on" property.
v = {k: self.last_on_value[k] for k in thing.channels}
if sum(v.values()) > 0:
self.channel_brightness(thing, v)
else:
self.channel_brightness(
thing,
{k: self.default_value[k] for k in thing.channels}
)
for thing2 in self.all_things:
t2v = [self.value[k] for k in thing2.channels]
logging.info(f'Dimmable_LED_strip_channels: {thing2.title}: {t2v}.')
if sum(t2v) > 0:
logging.info(f'Dimmable_LED_strip_channels: {thing2.title} to "ON".')
thing2.properties["on"].value.notify_of_external_update(True)
else:
logging.info(f'Dimmable_LED_strip_channels: {thing2.title} to "OFF".')
thing2.properties["on"].value.notify_of_external_update(False)
thing2.properties["brightness"].value.\
notify_of_external_update(scale(max(t2v), 100, "brightness"))
else:
# Switch LEDs OFF
# Notifications are handled by `reset`
self.reset(thing, {k: 0 for k in thing.channels})
def brightness(self, thing, value):
"""
"""
logging.info(f'Dimmable_LED_strip_channels: {self.channels} brightness to {value}.')
values = {k: self.value[k] for k in thing.channels}
if sum(values.values()) == 0:
values = {k: self.last_on_value[k] for k in thing.channels}
if sum(values.values()) == 0:
values = {k: self.default_value[k] for k in thing.channels}
scale = value / (100 * max(values.values()))
self.channel_brightness(thing, {k: v*scale for k, v in values.items()})
def colour(self, thing, value):
"""
"""
logging.info(f'Dimmable_LED_strip_channels: {self.channels} colour to {value} for {thing.title}, {thing.colour_type}.')
if thing.colour_type == "RGB":
self.channel_brightness(
thing,
{"Red": int(value[1:3], 16) / 255.0,
"Green": int(value[3:5], 16) / 255.0,
"Blue": int(value[5:7], 16) / 255.0,
}
)
elif thing.colour_type == "RGBW":
r = int(value[1:3], 16) / 255.0
g = int(value[3:5], 16) / 255.0
b = int(value[5:7], 16) / 255.0
w = min((r, g, b))
W = w * (1 - max((r, g, b, 0.0001)))
self.channel_brightness(
thing,
{"Red": r - W,
"Green": g - W,
"Blue": b - W,
"White": w,
}
)
elif thing.colour_type == "W":
r = int(value[1:3], 16) / 255.0
g = int(value[3:5], 16) / 255.0
b = int(value[5:7], 16) / 255.0
self.channel_brightness(thing, {"White": numpy.mean((r, g, b))})
@staticmethod
def __find_segment(value, curve):
"""
Find between which two keys `value` lies in the `curve` - which is a list of segments
This is the intended brightness.
Return the segment ends (key, value) pairs.
"""
pchannel_brightness, pPWM_duty_cycle = 0, 0
for channel_brightness in curve:
if value <= channel_brightness:
return [[pchannel_brightness, pPWM_duty_cycle], [channel_brightness, curve[channel_brightness]]]
pchannel_brightness, pPWM_duty_cycle = channel_brightness, curve[channel_brightness]
return [[pchannel_brightness, pPWM_duty_cycle], [1, 1]]
@staticmethod
def __apply_curve(value, curve):
"""
Calculate the PWM duty cycle we need to set based on the intended brightness
Return that value
"""
if type(curve) is int:
return value * (1 - curve) + curve
else:
((channel_brightness1, PWM_duty_cycle1), (channel_brightness2, PWM_duty_cycle2)) = \
Dimmable_LED_strip_channels.__find_segment(value, curve)
return PWM_duty_cycle1 + (value - channel_brightness1) * \
(PWM_duty_cycle2 - PWM_duty_cycle1) / \
(channel_brightness2 - channel_brightness1)
def __rectified_channel(self, value, channel_name):
"""
Calculate, scale and cap the PWM duty cycle we need to set based on the intended brightness
Return the scaled and capped value (as it must conform to the ADS device specification)
"""
return scale(self.__apply_curve(value, self.channel_curves[channel_name]), 0xfffe, "PWM " + channel_name)
def reset(self, thing, values):
"""
Set channel values to zero, remember last ON values, but only for the channels relevant to the calling
`webthing`.
If all channels are 0, then switch the relay OFF.
Notify all relevant changes to their `webthing`.
"""
updated_things = set()
# This is not a call to the self.set function, but the creation of a standard python set object (empty)
logging.info(f'Dimmable_LED_strip_channels: reset {thing.channels} to {values}.')
for channel_name in values.keys():
self.PWM_board.channels[self.channels[channel_name]].duty_cycle = 0
if self.value[channel_name] > 0:
self.last_on_value[channel_name] = self.value[channel_name]
self.value[channel_name] = 0
for thing1 in self.things[channel_name]:
updated_things.add(thing1)
for thing1 in updated_things:
t1v = {k: v for k, v in self.value.items() if k in thing1.channels}
thing1.properties["channel_brightness"].value.notify_of_external_update(t1v)
thing1.properties["colour"].value.notify_of_external_update(thing1.colour_convert(t1v))
thing1.properties["brightness"].value.notify_of_external_update(scale(max(t1v.values()), 100, "brightness"))
logging.info(f'Dimmable_LED_strip_channels: reset self.value = {self.value}.')
if sum(self.value.values()) == 0:
GPIO.output(self.on_off_channel, False)
for thing2 in self.all_things:
thing2.properties["on"].value.notify_of_external_update(False)
thing2.properties["brightness"].value.notify_of_external_update(scale(0, 100, "brightness"))
def channel_brightness(self, thing, values):
"""
Set channel values (maybe to zero), but only for the channels relevant to the calling `webthing`.
If all channels are 0, then switch the relay OFF.
If any channel is non 0, then switch the relay ON.
Notify all relevant changes to their `webthing`.
"""
updated_things = set()
# This is not a call to the self.set function, but the creation of a standard python set object (empty)
logging.info(f'Dimmable_LED_strip_channels: channel_brightness {thing.channels} to {values}.')
for channel_name, value in values.items():
if self.value[channel_name] != value:
for thing1 in self.things[channel_name]:
updated_things.add(thing1)
self.value[channel_name] = value
self.PWM_board.channels[self.channels[channel_name]].duty_cycle = self.__rectified_channel(value, channel_name)
for thing1 in updated_things:
t1v = {k: v for k, v in self.value.items() if k in thing1.channels}
thing1.properties["channel_brightness"].value.notify_of_external_update(t1v)
thing1.properties["colour"].value.notify_of_external_update(thing1.colour_convert(t1v))
thing1.properties["brightness"].value.notify_of_external_update(scale(max(t1v.values()), 100, "brightness"))
logging.info(f'Dimmable_LED_strip_channels: channel_brightness self.value = {self.value}.')
if sum(self.value.values()) == 0:
GPIO.output(self.on_off_channel, False)
for thing2 in self.all_things:
thing2.properties["on"].value.notify_of_external_update(False)
else:
GPIO.output(self.on_off_channel, True)
for thing2 in self.all_things:
if max([v for k, v in self.value.items() if k in thing2.channels]) > 0:
thing2.properties["on"].value.notify_of_external_update(True)
def channel_curve(self, value):
logging.info(f'Dimmable_LED_strip_channels: command to adjust channel curves to {value}.')
for c in value.keys():
self.channel_curves[c] = {float(k): v for k, v in value[c].items()}
for thing2 in self.all_things:
thing2.properties["channel_curve"].value.notify_of_external_update(self.channel_curves)
class Dimmable_LED_strip_webthing(Thing):
"""
This class is the logic layer presented to the web for consumption by webthing clients or a webthing gateway.
It wraps and is separate from the Hardware LED strip channels control because webthings can share one or more
channels.
"""
# Take example on:
# https://github.com/WebThingsIO/webthing-python/blob/5b779b5d3e545c93d636a0f6fac1582512cba62d/example/multiple-things.py#L161
def __init__(self, uritype, urilocation, uriname, name, description, channels, LED_strip_channels):
logging.info(f'{name}: initialising webthing.')
Thing.__init__(
self,
f'{uritype}.{urilocation}.{uriname}',
name,
['Light'],
description
)
self.LED_strip_channels = LED_strip_channels
self.channels = channels
rgb = len(set(channels) & {"Red", "Green", "Blue"}) > 0
w = len(set(channels) & {"White"}) > 0
if rgb & w:
self.colour_type = "RGBW"
elif rgb:
self.colour_type = "RGB"
elif w:
self.colour_type = "W"
else:
self.colour_type = "Other"
LED_strip_channels.register_thing_with_LED_strip_channels(self)