├── .gitignore ├── Makefile ├── README.md ├── lib └── readme.txt ├── misc ├── esplights.fzz ├── esplights.py ├── esplights_bb.png └── timings.yml ├── platformio.ini └── src ├── main.ino ├── transmit.c └── transmit.h /.gitignore: -------------------------------------------------------------------------------- 1 | .pioenvs/ 2 | .sconsign.dblite 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Uncomment lines below if you have problems with $PATH 2 | #SHELL := /bin/bash 3 | #PATH := /usr/local/bin:$(PATH) 4 | 5 | all: 6 | platformio -f -c vim run --target upload 7 | 8 | clean: 9 | platformio -f -c vim run --target clean 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESPlights 2 | 3 | This is a home automation controller and sensor that I wrote for my house. 4 | 5 | It's mostly ad-hoc, but hopefully you'll find something here that will help you. 6 | 7 | ## Components 8 | 9 | To make everything work, you need: 10 | 11 | * A NodeMCU (that's what I used) or another compatible ESP8266 thing. An Arduino 12 | would probably also work. 13 | * An infrared LED. 14 | * A 433 MHz RF transmitter. 15 | * An alarm (I used one of those magnetic window alarm thingies). 16 | * An analog light sensor. 17 | * A DHT11 temperature/humidity sensor. 18 | * A pyroelectric IR motion sensor. 19 | 20 | There are multiple components to this project, which will be explained below. 21 | 22 | 23 | ### A wifi-enabled MQTT client 24 | 25 | I use the ESP8266 to connect to the home wifi network and from that to my MQTT 26 | server, which allows all the below functionality to work. All the sensors and 27 | transmitters report to and receive commands from MQTT topics. 28 | 29 | 30 | ### An RF transmitter 31 | 32 | I bought some [RF-controlled 33 | sockets](http://www.alibaba.com/product-detail/remote-control-socket-wireless-socket-rf_60038866245/showimage.html) 34 | and decided to automate some lights in my house, so 35 | I [reverse-engineered](http://www.stavros.io/posts/how-remote-control-rf-devices-raspberry-pi/) 36 | the protocol and wrote some code to [control the lights 37 | remotely](http://www.stavros.io/posts/control-rf-devices-with-arduino/). This is 38 | why you need the RF transmitter. If you don't have those sockets, you can just 39 | ignore that part of the code. 40 | 41 | 42 | ### A motion sensor 43 | 44 | The motion sensor is used for the lights above, it turns them off if there's no 45 | motion in X minutes and turns them back on if motion is detected. 46 | 47 | 48 | ### Light, temperature and humidity sensors 49 | 50 | The light sensor is used for the lights too. If there's no light and there has 51 | been motion lately, the lamp turns on, and it turns off if there's a lot of 52 | light detected suddenly (it means that I turned some other light on). 53 | 54 | The temperature and humidity sensors are just for fun. 55 | 56 | 57 | ### An alarm 58 | 59 | The alarm is triggered if I have indicated that I'm not at home and motion is 60 | detected. I also get a message on my phone, but all this is done with an 61 | external script that is not included here. 62 | 63 | 64 | ### A universal IR remote control 65 | 66 | Since I had this, I figured I'd write some code to control my TV and AC unit. 67 | There's a helper script, `misc/esplights.py` that will: 68 | 69 | * Transform timings, options, parameters, etc passed on the command line into a 70 | suitable format for the ESPlights controller to read. 71 | * Enable and disable automation (whether lights are turned on or off 72 | automatically). This only lasts for a few hours before it times out. 73 | * Activate and deactivate the alarm. 74 | * Help you analyze timings. Pass it a CSV from a Saleae Logic export and it will 75 | print timings compatible with itself (for analyzing IR and RF signals). 76 | 77 | The remote control protocol allows you to send any RF or IR signal (the pin is 78 | selectable), and optionally add a carrier wave. The `duty_high` and `duty_low` 79 | parameters define the duty cycle (in μs), and a low duty cycle of 0 means no 80 | carrier (the timings will sent on a fully-high) signal. This allows you to 81 | modulate everything from RF to AC to TV (all of which use different or no 82 | carriers) through one protocol. 83 | 84 | 85 | ### A sensor aggregator 86 | 87 | ESPlights will send its sensor readings to an MQTT topic once a second. I just 88 | plot them somewhere so I know what's going on, but you can do whatever. I also 89 | have an XMPP bot that I can talk to and get a reading of the sensors if I'm on 90 | my mobile. Go nuts. 91 | 92 | 93 | ## Cetera 94 | 95 | That's about it for this repo. It also supports OTA updates, which are very 96 | useful if you don't want to be connecting the sensor to your computer to update 97 | it. Just set the key in the definitions at the top of the source file and you're 98 | good to flash using espota. 99 | 100 | If you have any feedback or whatever, open an issue or (even better) issue a 101 | pull request. I can't guarantee that I'll merge it, and I'm a bit of a dick, so 102 | bear with me. 103 | 104 | There's also a Fritzing file of roughly how the connections go, but it's not 105 | complete because my components had the wrong pinouts and I couldn't find some of 106 | the components I'm actually using. Feel free to update it and issue a PR. 107 | 108 | Here's what it looks like: 109 | 110 | ![The breadboard](misc/esplights_bb.png) 111 | 112 | 113 | ## License 114 | 115 | This repository is released under the GPL v3. 116 | -------------------------------------------------------------------------------- /lib/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for the project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link to executable file. 4 | 5 | The source code of each library should be placed in separate directory, like 6 | "lib/private_lib/[here are source files]". 7 | 8 | For example, see how can be organised `Foo` and `Bar` libraries: 9 | 10 | |--lib 11 | | |--Bar 12 | | | |--docs 13 | | | |--examples 14 | | | |--src 15 | | | |- Bar.c 16 | | | |- Bar.h 17 | | |--Foo 18 | | | |- Foo.c 19 | | | |- Foo.h 20 | | |- readme.txt --> THIS FILE 21 | |- platformio.ini 22 | |--src 23 | |- main.c 24 | 25 | Then in `src/main.c` you should use: 26 | 27 | #include 28 | #include 29 | 30 | // rest H/C/CPP code 31 | 32 | PlatformIO will find your libraries automatically, configure preprocessor's 33 | include paths and build them. 34 | 35 | See additional options for PlatformIO Library Dependency Finder `lib_*`: 36 | 37 | http://docs.platformio.org/en/latest/projectconf.html#lib-install 38 | 39 | -------------------------------------------------------------------------------- /misc/esplights.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skorokithakis/esplights/f97dbdd1749189e36aeb65987402100d54ff2336/misc/esplights.fzz -------------------------------------------------------------------------------- /misc/esplights.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Usage: 3 | arduirc.py [options] send (
| ) 4 | arduirc.py analyze 5 | arduirc.py automation ( enable | disable ) 6 | arduirc.py alarm ( enable | disable ) 7 | 8 | Options: 9 | -h --help show this help and exit 10 | -s --server=HOSTNAME the MQTT server to connect to [default: localhost]. 11 | -u --duty-high=USEC the high part of the duty cycle in usec (0-255) [default: 26] 12 | -l --duty-low=USEC the low part of the duty cycle in usec (0-255) [default: 0] 13 | -v --version show version and exit 14 | -t --timings-file=FILE specify the timings file to use [default: timings.yml] 15 | -w --wait wait two seconds for the Arduino to reset. 16 | -r --repeat=REPEAT repeat the command REPEAT times [default: 3] 17 | -d --delay=DELAY delay between repeats (in usec) [default: 10000] 18 | -p --pin=PIN which GPIO pin to write to [default: 4] 19 | """ 20 | 21 | import csv 22 | import sys 23 | import yaml 24 | import struct 25 | 26 | from docopt import docopt 27 | import paho.mqtt.client as mqtt 28 | 29 | 30 | def mqtt_send(server, payload): 31 | # chr(1) is the command (command 1, send timings). 32 | client = mqtt.Client() 33 | client.connect(server) 34 | client.publish("esplights_command", bytearray(payload)) 35 | 36 | 37 | def send(arguments): 38 | # Get the timings. 39 | pin = int(arguments["--pin"]) 40 | repeat = int(arguments["--repeat"]) 41 | delay = int(round(int(arguments["--delay"]) / 100.0)) 42 | duty_high = max(1, int(arguments["--duty-high"]) + 1) 43 | duty_low = max(1, int(arguments["--duty-low"]) + 1) 44 | 45 | if arguments.get("
") and arguments.get(""): 46 | section = arguments["
"] 47 | command = arguments[""] 48 | 49 | try: 50 | timing_dict = yaml.load(open(arguments["--timings-file"])) 51 | except IOError: 52 | sys.exit("Error opening timings file.") 53 | 54 | if section not in timing_dict: 55 | sys.exit("Unknown section.") 56 | 57 | if command not in timing_dict[section]["timings"]: 58 | sys.exit("Unknown command.") 59 | 60 | raw_timings = timing_dict[section]["timings"][command] 61 | 62 | # Override the command line with the parameters in the file. 63 | params = timing_dict[section].get("parameters", {}) 64 | pin = params.get("pin", pin) 65 | duty_high = params.get("duty_high", duty_high) 66 | duty_low = params.get("duty_low", duty_low) 67 | repeat = params.get("repeat", repeat) 68 | delay = params.get("delay", delay) 69 | 70 | elif arguments.get(""): 71 | raw_timings = arguments[""] 72 | 73 | timings = [int(timing) for timing in raw_timings.split()] 74 | 75 | output = struct.pack("!" + ("h" * len(timings)), *timings) 76 | output = output.replace(chr(0), chr(1)) # Can't have null bytes. 77 | 78 | if delay > 255 or delay < 1: 79 | sys.exit("Delay must be between 100 and 25500.") 80 | 81 | if pin > 13 or pin < 0: 82 | sys.exit("Pin must be between 0 and 13.") 83 | 84 | if repeat > 255 or repeat < 1: 85 | sys.exit("Repeat must be between 1 and 255.") 86 | 87 | output = chr(1) + chr(duty_high) + chr(duty_low) + chr(pin) + chr(repeat) + chr(delay) + output + chr(0) 88 | mqtt_send(arguments["--server"], output) 89 | 90 | print("Command sent.") 91 | 92 | 93 | def automation(enable): 94 | if arguments["enable"]: 95 | mqtt_send(arguments["--server"], "auto on") 96 | else: 97 | mqtt_send(arguments["--server"], "auto off") 98 | print("Command sent.") 99 | 100 | 101 | def alarm(enable): 102 | if arguments["enable"]: 103 | mqtt_send(arguments["--server"], "alarm on") 104 | else: 105 | mqtt_send(arguments["--server"], "alarm off") 106 | print("Command sent.") 107 | 108 | 109 | def analyze(arguments): 110 | """ 111 | Read a CSV file from a logic analyzer and print out a send-compatible 112 | timings list. 113 | """ 114 | reader = csv.reader(open(arguments[""])) 115 | 116 | # Read and convert seconds to microseconds. 117 | timings = [int(1000000 * float(x[0])) for x in reader] 118 | 119 | # Normalize to 0 as the first value. 120 | timings = [x - timings[0] for x in timings] 121 | 122 | # Calculate the differences. 123 | diffs = [x[0] - x[1] for x in zip(timings[1:], timings)] 124 | print "Timings: %s" % " ".join([str(x) for x in diffs]) 125 | 126 | 127 | def main(arguments): 128 | if arguments["send"]: 129 | send(arguments) 130 | elif arguments["automation"]: 131 | automation(arguments) 132 | elif arguments["alarm"]: 133 | alarm(arguments) 134 | elif arguments["analyze"]: 135 | analyze(arguments) 136 | 137 | 138 | if __name__ == "__main__": 139 | arguments = docopt(__doc__, version="0.1.0") 140 | main(arguments) 141 | -------------------------------------------------------------------------------- /misc/esplights_bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skorokithakis/esplights/f97dbdd1749189e36aeb65987402100d54ff2336/misc/esplights_bb.png -------------------------------------------------------------------------------- /misc/timings.yml: -------------------------------------------------------------------------------- 1 | sockets: 2 | parameters: 3 | repeat: 3 4 | # If duty_low is 0, there's no carrier frequency, and duty_high is ignored. 5 | duty_low: 0 6 | timings: 7 | on1: 604 646 1229 667 1208 1312 583 8 | off1: 625 625 1250 667 1250 667 1250 9 | allon: 625 625 1312 667 1250 667 1271 10 | alloff: 646 625 1312 687 1208 1312 583 11 | tv: 12 | # LG TV timings. 13 | parameters: 14 | repeat: 1 15 | pin: 13 16 | duty_high: 19 17 | duty_low: 7 18 | timings: 19 | volup: 9047 4506 629 510 594 540 595 1662 602 537 593 543 593 542 562 574 562 575 592 1663 625 1635 624 513 593 1663 600 1658 625 1634 624 1633 626 1633 597 541 561 1695 596 541 562 576 560 575 562 575 561 573 563 574 562 1695 596 540 563 1694 596 1663 595 1663 596 1662 596 1662 595 1664 595 20 | voldown: 9118 4433 635 506 631 505 632 1624 633 505 631 504 632 505 632 504 631 506 630 1624 633 1626 633 505 631 1625 633 1625 633 1625 633 1625 633 1625 633 1625 633 1624 635 504 632 505 631 505 631 505 631 505 631 505 631 505 631 505 632 1624 633 1625 634 1624 634 1625 633 1624 634 1624 634 21 | chup: 9088 4472 595 545 592 545 585 1670 596 544 588 548 588 547 591 546 591 545 587 1669 594 1663 595 545 621 1634 594 1664 621 1636 624 1636 595 1663 624 515 620 517 620 516 622 514 619 517 621 515 624 513 592 544 620 1634 625 1634 626 1632 626 1633 624 1635 624 1633 625 1634 625 1633 626 22 | chdown: 9121 4436 630 509 628 508 628 1627 631 508 629 507 629 507 629 508 628 507 630 1626 631 1627 631 508 629 1627 630 1629 629 1629 630 1628 630 1627 631 1629 630 507 630 506 630 507 630 506 629 507 630 506 632 505 629 507 630 1626 631 1627 632 1627 631 1626 633 1627 631 1626 633 1627 631 23 | "on": 9112 4444 629 512 623 513 623 1633 627 511 625 512 623 513 622 515 622 513 623 1633 627 1631 628 511 623 1633 628 1629 628 1631 628 1629 628 1632 626 513 623 513 622 514 623 1632 628 511 624 513 623 513 622 514 624 1631 628 1632 627 1631 627 512 622 1632 627 1632 626 1633 626 1632 627 24 | left: 9116 4443 619 520 622 514 621 1635 594 544 622 515 624 512 622 514 622 515 620 1635 623 1635 621 518 621 1636 619 1639 621 1638 621 1636 621 1638 621 1637 622 1637 622 1635 625 515 622 514 621 514 624 513 622 514 622 514 620 517 621 515 622 1634 621 1636 623 1636 621 1637 624 1634 624 25 | right: 9113 4445 624 516 590 546 618 1638 594 545 619 517 589 548 617 518 620 517 618 1637 595 1664 593 546 600 1656 592 1667 591 1667 591 1667 592 1667 594 544 619 1637 595 1664 606 532 620 516 621 516 622 514 620 516 621 1636 620 518 620 517 621 1635 621 1636 621 1637 624 1636 593 1667 620 26 | up: 9112 4445 565 574 562 572 564 1695 564 572 564 572 564 573 563 575 562 572 564 1694 564 1696 563 574 562 1694 564 1696 562 1695 564 1694 564 1695 563 574 562 573 564 572 563 574 562 574 563 572 564 1695 564 572 563 1696 563 1695 563 1695 563 1696 563 1695 563 1695 563 573 563 1696 562 27 | down: 9114 4443 627 513 591 546 618 1637 593 547 618 518 618 518 618 518 618 519 618 1637 593 1664 595 545 593 1663 593 1666 593 1665 591 1667 595 1663 594 1665 595 544 620 516 619 517 620 516 621 515 623 1634 619 519 621 516 621 1635 621 1639 604 1652 621 1639 593 1665 621 517 623 1634 621 28 | ok: 9111 4447 564 574 562 572 564 1695 564 573 563 573 563 574 562 573 563 574 563 1695 563 1695 563 574 562 1696 563 1695 563 1695 564 1696 563 1694 564 574 562 573 563 1695 564 574 562 572 564 574 563 1693 565 573 563 1695 564 1694 564 575 561 1695 593 1666 563 1694 564 574 563 1694 564 29 | input: 9108 4447 564 573 613 522 616 1643 564 572 614 522 616 520 614 522 616 520 618 1641 563 1695 564 572 614 1644 563 1695 565 1694 563 1695 564 1693 566 1694 564 1693 565 573 613 1644 564 572 613 524 614 521 617 520 615 520 616 521 615 1642 565 573 613 1645 563 1695 563 1694 564 1695 563 30 | back: 9116 4438 635 505 631 504 632 1625 634 504 632 504 632 504 632 504 632 504 632 1624 634 1624 634 504 632 1625 633 1625 633 1624 635 1624 634 1625 633 505 631 504 632 505 631 1625 633 504 631 1626 633 505 631 504 632 1625 633 1625 633 1626 632 505 631 1625 633 505 631 1625 633 1624 634 31 | mute: 9119 4434 634 506 631 504 632 1625 633 505 632 503 632 505 632 504 632 504 631 1625 634 1624 634 505 631 1625 633 1625 634 1623 634 1624 635 1624 634 1625 633 506 631 505 631 1624 634 505 632 504 632 504 632 505 631 504 632 1624 634 1625 633 505 631 1625 633 1626 632 1625 634 1626 632 32 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [env:esp12e] 2 | platform = espressif 3 | framework = arduino 4 | board = esp12e 5 | targets = upload 6 | lib_install = 19,89 7 | build_flags = -DMQTT_MAX_PACKET_SIZE=1024 8 | -------------------------------------------------------------------------------- /src/main.ino: -------------------------------------------------------------------------------- 1 | #include "PubSubClient.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "transmit.h" 9 | 10 | #ifndef WIFI_SSID 11 | #define WIFI_SSID "your ssid" 12 | #endif 13 | 14 | #ifndef WIFI_PASS 15 | #define WIFI_PASS "your wifi password" 16 | #endif 17 | 18 | #ifndef OTA_PASS 19 | #define OTA_PASS "an ota pass" 20 | #endif 21 | 22 | #define MQTT_ENABLED 1 23 | 24 | #ifndef MQTT_SERVER 25 | #define MQTT_SERVER "test.mosquitto.org" 26 | #endif 27 | 28 | #define MQTT_PORT 1883 29 | 30 | #ifndef NTP_SERVER 31 | #define NTP_SERVER "pool.ntp.org" 32 | #endif 33 | 34 | #define ACTIVE_MOTION_THRESHOLD 10 * 60 35 | #define LIGHT_ON_THRESHOLD 90 36 | #define LIGHT_OFF_THRESHOLD 200 37 | 38 | #define MOTION_PIN D7 39 | #define TEMPERATURE_PIN D6 40 | #define LED_PIN D4 41 | #define LED2_PIN D0 42 | #define RF_PIN D1 43 | #define ALARM_PIN D5 44 | #define LIGHT_PIN A0 45 | 46 | #define UPDATE_INTERVAL_MS 200 47 | #define PUBLISH_STATE_MS 1000 48 | #define MAX_MESSAGE_LENGTH 600 49 | #define DELAY_MULTIPLIER 100 50 | #define UNAUTO_DELAY 13 * 60 * 60L 51 | 52 | #define CONFIG_VERSION "cf1" 53 | 54 | WiFiClient wclient; 55 | char COMMAND_CHANNEL[] = "esplights_command"; 56 | char STATE_CHANNEL[] = "esplights_state"; 57 | char LOG_CHANNEL[] = "esplights_log"; 58 | String NAME = "esplights1"; 59 | 60 | enum {OTA, CONNECTING_WIFI, CONNECTING_MQTT, CONNECTED} state = CONNECTING_WIFI; 61 | 62 | #if MQTT_ENABLED == 1 63 | PubSubClient client(wclient); 64 | #endif 65 | 66 | struct Sensors { 67 | unsigned long motion = 0; 68 | unsigned long motionTime = 0; 69 | unsigned int motionCounter = 0; 70 | unsigned int humidity = 0; 71 | unsigned int temperature = 0; 72 | unsigned int lightLevel = 0; 73 | } sensors; 74 | 75 | struct Persistent { 76 | char version[4] = CONFIG_VERSION; 77 | unsigned long autoDisableTime = 0; 78 | char automation = 1; 79 | } persistent; 80 | 81 | struct Misc { 82 | unsigned int lastUpdate = 0; 83 | unsigned int lastShow = 0; 84 | unsigned int alarm = 0; 85 | unsigned long bootTime = 0; 86 | char lampState = 0; 87 | } misc; 88 | 89 | unsigned char messageOn[] = {2, 113, 2, 113, 5, 32, 2, 113, 5, 12, 2, 113, 5, 12, 2, 113, 5, 32, 2, 113, 5, 12, 2, 134, 5, 12, 2, 113, 5, 12, 2, 134, 4, 247, 2, 155, 4, 247, 2, 134, 4, 247, 2, 134, 5, 12, 2, 134, 4, 247, 2, 155, 4, 226, 5, 12, 2, 92, 2, 155, 4, 205, 5, 12, 2, 92, 2, 155, 4, 247, 2, 134, 4, 247, 2, 134, 4, 247, 2, 155, 4, 226}; 90 | unsigned char messageOff[] = {2, 113, 2, 113, 5, 32, 2, 113, 5, 12, 2, 113, 5, 32, 2, 92, 5, 32, 2, 134, 5, 12, 2, 113, 5, 12, 2, 113, 5, 12, 2, 134, 4, 247, 2, 155, 4, 247, 2, 134, 5, 12, 2, 134, 4, 247, 2, 134, 4, 247, 2, 155, 4, 247, 5, 53, 2, 71, 2, 155, 4, 247, 2, 134, 4, 247, 2, 155, 4, 226, 2, 155, 4, 205, 2, 175, 4, 163, 5, 53, 2, 50}; 91 | 92 | 93 | // Publish a message to MQTT if connected. 94 | void mqttPublish(String topic, String payload) { 95 | #if MQTT_ENABLED == 1 96 | if (!client.connected()) { 97 | return; 98 | } 99 | client.publish(topic.c_str(), payload.c_str()); 100 | #endif 101 | } 102 | 103 | 104 | // Receive a message from MQTT and act on it. 105 | #if MQTT_ENABLED == 1 106 | void mqttCallback(char* chTopic, byte* chPayload, unsigned int length) { 107 | chPayload[length] = '\0'; 108 | String payload = String((char*)chPayload); 109 | 110 | if (payload == "auto on") { 111 | enableAuto(1); 112 | } else if (payload == "auto off") { 113 | enableAuto(0); 114 | } else if (payload == "alarm on") { 115 | misc.alarm = 1; 116 | digitalWrite(ALARM_PIN, HIGH); 117 | } else if (payload == "alarm off") { 118 | misc.alarm = 0; 119 | digitalWrite(ALARM_PIN, LOW); 120 | } else if (payload[0] == 1) { 121 | cmdSend(chPayload[3], &chPayload[6], chPayload[4], chPayload[5] * DELAY_MULTIPLIER, chPayload[1] - 1, chPayload[2] - 1); 122 | } 123 | } 124 | #endif 125 | 126 | 127 | // Send a command, printing some debug information. 128 | void cmdSend(char pin, unsigned char message[], unsigned char repeat, unsigned int intraDelay, unsigned char dcHigh, unsigned char dcLow) { 129 | unsigned int out = 0; 130 | unsigned int i; 131 | 132 | digitalWrite(LED2_PIN, LOW); 133 | 134 | Serial.print("Sending on pin "); 135 | Serial.print(pin, DEC); 136 | Serial.print(", repeating "); 137 | Serial.print(repeat, DEC); 138 | Serial.print(" times, for "); 139 | Serial.print(intraDelay); 140 | Serial.print(" with a high duty cycle of "); 141 | Serial.print(dcHigh, DEC); 142 | Serial.print(" and a low duty cycle of "); 143 | Serial.print(dcLow, DEC); 144 | Serial.println("."); 145 | 146 | // Print the received timings to the console. 147 | for (i = 0; i < MAX_MESSAGE_LENGTH; i = i + 2) { 148 | if (message[i] == 0 || message[i+1] == 0) { 149 | break; 150 | } 151 | out = (message[i] << 8) + message[i+1]; 152 | Serial.print(out, DEC); 153 | Serial.print(" "); 154 | } 155 | Serial.println(""); 156 | yield(); 157 | 158 | transmit(pin, message, repeat, intraDelay, dcHigh, dcLow); 159 | 160 | Serial.println("Command sent."); 161 | digitalWrite(LED2_PIN, HIGH); 162 | } 163 | 164 | 165 | unsigned long getNTPTime() { 166 | IPAddress address; 167 | WiFiUDP udp; 168 | const int NTP_PACKET_SIZE = 48; 169 | byte packetBuffer[NTP_PACKET_SIZE]; 170 | 171 | udp.begin(2390); 172 | // set all bytes in the buffer to 0 173 | memset(packetBuffer, 0, NTP_PACKET_SIZE); 174 | // Initialize values needed to form NTP request 175 | // (see URL above for details on the packets) 176 | packetBuffer[0] = 0b11100011; // LI, Version, Mode 177 | packetBuffer[1] = 0; // Stratum, or type of clock 178 | packetBuffer[2] = 6; // Polling Interval 179 | packetBuffer[3] = 0xEC; // Peer Clock Precision 180 | // 8 bytes of zero for Root Delay & Root Dispersion 181 | packetBuffer[12] = 49; 182 | packetBuffer[13] = 0x4E; 183 | packetBuffer[14] = 49; 184 | packetBuffer[15] = 52; 185 | 186 | // all NTP fields have been given values, now 187 | // you can send a packet requesting a timestamp: 188 | WiFi.hostByName(NTP_SERVER, address); 189 | udp.beginPacket(address, 123); //NTP requests are to port 123 190 | udp.write(packetBuffer, NTP_PACKET_SIZE); 191 | udp.endPacket(); 192 | 193 | delay(1000); 194 | 195 | int cb = udp.parsePacket(); 196 | if (!cb) { 197 | return 0; 198 | } else { 199 | udp.read(packetBuffer, NTP_PACKET_SIZE); 200 | unsigned long highWord = word(packetBuffer[40], packetBuffer[41]); 201 | unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]); 202 | unsigned long secsSince1900 = highWord << 16 | lowWord; 203 | const unsigned long seventyYears = 2208988800UL; 204 | unsigned long epoch = secsSince1900 - seventyYears; 205 | return epoch; 206 | } 207 | } 208 | 209 | 210 | void resetPins() { 211 | pinMode(1, OUTPUT); 212 | digitalWrite(1, LOW); 213 | pinMode(2, OUTPUT); 214 | digitalWrite(2, LOW); 215 | pinMode(3, OUTPUT); 216 | digitalWrite(3, LOW); 217 | pinMode(4, OUTPUT); 218 | digitalWrite(4, LOW); 219 | pinMode(5, OUTPUT); 220 | digitalWrite(5, LOW); 221 | pinMode(12, OUTPUT); 222 | digitalWrite(12, LOW); 223 | pinMode(13, OUTPUT); 224 | digitalWrite(13, LOW); 225 | pinMode(14, OUTPUT); 226 | digitalWrite(14, LOW); 227 | pinMode(15, OUTPUT); 228 | digitalWrite(15, LOW); 229 | } 230 | 231 | void loadState() { 232 | if (EEPROM.read(0) == CONFIG_VERSION[0] && 233 | EEPROM.read(1) == CONFIG_VERSION[1] && 234 | EEPROM.read(2) == CONFIG_VERSION[2]) { 235 | for (unsigned int t=0; t persistent.autoDisableTime))) { 267 | enableAuto(1); 268 | } 269 | } 270 | 271 | 272 | // Return a JSON string of the entire state of the inputs. 273 | String sensorState() { 274 | return String(String("{\n") + 275 | "\"LAST_MOTION_SEC\": " + String((unsigned long)((millis() / 1000) - sensors.motionTime), DEC) + ",\n" + 276 | "\"MOTION_COUNTER\": " + String(sensors.motionCounter, DEC) + ",\n" + 277 | "\"ALARM\": " + String(misc.alarm, DEC) + ",\n" + 278 | "\"HUMIDITY\": " + String(sensors.humidity, DEC) + ",\n" + 279 | "\"TEMPERATURE\": " + String(sensors.temperature, DEC) + ",\n" + 280 | "\"LIGHT_LEVEL\": " + String(sensors.lightLevel, DEC) + ",\n" + 281 | "\"LAMP_STATE\": " + String(misc.lampState, DEC) + ",\n" + 282 | "\"AUTOMATION\": " + String(persistent.automation, DEC) + ",\n" + 283 | "\"AUTO_DISABLE_TIME\": " + String(persistent.autoDisableTime, DEC) + ",\n" + 284 | "\"CURRENT_TIME\": " + String(currentTime(), DEC) + ",\n" + 285 | "}\n"); 286 | } 287 | 288 | 289 | // Decide whether to control the light. 290 | void decide() { 291 | unsigned long lastMotionSec = (millis() / 1000) - sensors.motionTime; 292 | unsigned int lightLevel = analogRead(LIGHT_PIN); 293 | 294 | if (persistent.automation == 0) { 295 | // Do nothing if we aren't managing the thing. 296 | return; 297 | } 298 | 299 | if ((lastMotionSec < ACTIVE_MOTION_THRESHOLD) && 300 | (sensors.lightLevel < LIGHT_ON_THRESHOLD) && 301 | (misc.lampState == 0)) { 302 | // Turn on if motion is detected and there is no light. 303 | cmdSend(RF_PIN, messageOn, 3, 10000, 0, 0); 304 | misc.lampState = 1; 305 | mqttPublish(LOG_CHANNEL, "Turning on..."); 306 | } else if ((lastMotionSec >= ACTIVE_MOTION_THRESHOLD) && 307 | (misc.lampState == 1)) { 308 | // Turn off if motion is not detected for some time. 309 | cmdSend(RF_PIN, messageOff, 3, 10000, 0, 0); 310 | misc.lampState = 0; 311 | mqttPublish(LOG_CHANNEL, "Turning off because of motion..."); 312 | } else if ((sensors.lightLevel > LIGHT_OFF_THRESHOLD) && (misc.lampState == 1)) { 313 | // Turn off if other lights are turned on. 314 | cmdSend(RF_PIN, messageOff, 3, 10000, 0, 0); 315 | misc.lampState = 0; 316 | mqttPublish(LOG_CHANNEL, "Turning off because of lights..."); 317 | } 318 | } 319 | 320 | 321 | // Update the sensors to their latest values. 322 | void updateSensors() { 323 | char motionInput = 0; 324 | 325 | motionInput = digitalRead(MOTION_PIN); 326 | digitalWrite(LED_PIN, !motionInput); 327 | 328 | // Check last update. 329 | if (millis() > misc.lastUpdate + UPDATE_INTERVAL_MS) { 330 | misc.lastUpdate = millis(); 331 | } else { 332 | return; 333 | } 334 | 335 | // Check motion. 336 | Serial.println(motionInput, DEC); 337 | 338 | if (motionInput) { 339 | sensors.motionTime = millis() / 1000; 340 | } 341 | 342 | if ((motionInput == 1) && (sensors.motion == 0)) { 343 | sensors.motionCounter++; 344 | } 345 | sensors.motion = motionInput; 346 | sensors.lightLevel = analogRead(LIGHT_PIN); 347 | 348 | // We don't need to read this every second. 349 | if (millis() % 10 == 0) { 350 | DHT sensor(TEMPERATURE_PIN, DHT11); 351 | 352 | sensors.humidity = sensor.readHumidity(); 353 | sensors.temperature = sensor.readTemperature(); 354 | } 355 | decide(); 356 | } 357 | 358 | 359 | // Publish our current sensor readings to MQTT. 360 | void publishState() { 361 | // Check last update. 362 | if (millis() > misc.lastShow + PUBLISH_STATE_MS) { 363 | misc.lastShow = millis(); 364 | } else { 365 | return; 366 | } 367 | 368 | mqttPublish(STATE_CHANNEL, sensorState()); 369 | } 370 | 371 | 372 | // Check the MQTT connection and reboot if we can't connect. 373 | void connectMQTT() { 374 | #if MQTT_ENABLED == 1 375 | if (state == OTA) return; 376 | 377 | if (client.connected()) { 378 | client.loop(); 379 | state = CONNECTED; 380 | } else { 381 | state = CONNECTING_MQTT; 382 | 383 | int retries = 4; 384 | Serial.println("\nConnecting to MQTT..."); 385 | while (!client.connect(NAME.c_str()) && retries--) { 386 | delay(500); 387 | Serial.println("Retry..."); 388 | } 389 | 390 | if (!client.connected()) { 391 | Serial.println("\nfatal: MQTT server connection failed. Rebooting."); 392 | delay(200); 393 | ESP.restart(); 394 | } 395 | 396 | Serial.println("Connected."); 397 | client.subscribe(COMMAND_CHANNEL); 398 | } 399 | #endif 400 | } 401 | 402 | 403 | // Check the WiFi connection and connect if it's down. 404 | void connectWifi() { 405 | if (WiFi.waitForConnectResult() != WL_CONNECTED) { 406 | state = CONNECTING_WIFI; 407 | 408 | WiFi.mode(WIFI_STA); 409 | while (WiFi.waitForConnectResult() != WL_CONNECTED) { 410 | WiFi.begin(WIFI_SSID, WIFI_PASS); 411 | Serial.println("Connecting to wifi..."); 412 | } 413 | 414 | Serial.print("Wifi connected, IP address: "); 415 | Serial.println(WiFi.localIP()); 416 | } 417 | 418 | state = CONNECTING_MQTT; 419 | } 420 | 421 | 422 | void setup() { 423 | resetPins(); 424 | Serial.begin(115200); 425 | EEPROM.begin(32); 426 | loadState(); 427 | 428 | pinMode(MOTION_PIN, INPUT); 429 | pinMode(LIGHT_PIN, INPUT); 430 | pinMode(LED_PIN, OUTPUT); 431 | pinMode(LED2_PIN, OUTPUT); 432 | pinMode(RF_PIN, OUTPUT); 433 | pinMode(ALARM_PIN, OUTPUT); 434 | digitalWrite(LED2_PIN, HIGH); 435 | digitalWrite(ALARM_PIN, LOW); 436 | 437 | #if MQTT_ENABLED == 1 438 | client.setServer(MQTT_SERVER, MQTT_PORT); 439 | client.setCallback(mqttCallback); 440 | #endif 441 | 442 | connectWifi(); 443 | connectMQTT(); 444 | state = CONNECTED; 445 | 446 | misc.bootTime = getNTPTime() - (millis() / 1000); 447 | 448 | // Enable OTA updates. 449 | ArduinoOTA.setPassword((const char *) OTA_PASS); 450 | ArduinoOTA.setHostname("ESPlights"); 451 | 452 | ArduinoOTA.onStart([]() { 453 | Serial.println("Start"); 454 | state = OTA; 455 | }); 456 | ArduinoOTA.onEnd([]() { 457 | Serial.println("\nEnd"); 458 | }); 459 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 460 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 461 | }); 462 | ArduinoOTA.onError([](ota_error_t error) { 463 | Serial.printf("Error[%u]: ", error); 464 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 465 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 466 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 467 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 468 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); 469 | }); 470 | ArduinoOTA.begin(); 471 | } 472 | 473 | 474 | void loop() { 475 | connectWifi(); 476 | connectMQTT(); 477 | 478 | state = CONNECTED; 479 | 480 | checkTimers(); 481 | updateSensors(); 482 | publishState(); 483 | 484 | ArduinoOTA.handle(); 485 | } 486 | -------------------------------------------------------------------------------- /src/transmit.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define CPU_LATENCY 1 4 | #define MAX_MESSAGE_LENGTH 600 5 | 6 | 7 | // Transmit a command. 8 | // pin: The pin to transmit to. 9 | // message[[]: The 2-byte timings to send. 10 | // repeat: How many times to repeat the message. 11 | // intraDelay: How long to wait between repeats. 12 | // dcHigh: How long the high duty cycle is (in microseconds). 13 | // Anything over 24 means 100% duty cycle. Will have a constant 14 | // added to it due to processing latency. 15 | // dcLow: How long the low duty cycle is (in microseconds). 16 | void ICACHE_RAM_ATTR transmit(char pin, unsigned char message[], unsigned char repeat, unsigned int intraDelay, unsigned char dcHigh, unsigned char dcLow) { 17 | int status = LOW; 18 | unsigned int out = 0; 19 | unsigned int i, j; 20 | unsigned int k; 21 | unsigned int dcTotal = dcHigh + dcLow; 22 | bool carrier = true; 23 | 24 | // Compensate for CPU latency. 25 | 26 | if (dcHigh < CPU_LATENCY) { 27 | dcHigh = 0; 28 | } else { 29 | dcHigh = dcHigh - CPU_LATENCY; 30 | } 31 | 32 | if (dcLow < CPU_LATENCY) { 33 | dcLow = 0; 34 | } else { 35 | dcLow = dcLow - CPU_LATENCY; 36 | } 37 | 38 | pinMode(pin, OUTPUT); 39 | digitalWrite(pin, LOW); 40 | 41 | if (dcLow == 0) { 42 | // If the low time is 0, there's no carrier frequency. 43 | carrier = false; 44 | } 45 | 46 | // Send the message. 47 | for (j = 0; j < repeat; j++) { 48 | for (i = 0; i < MAX_MESSAGE_LENGTH; i = i + 2) { 49 | if (message[i] == 0 || message[i+1] == 0) { 50 | break; 51 | } 52 | 53 | // Convert two bytes to an int. 54 | out = (message[i] << 8) + message[i+1]; 55 | if (status == HIGH) { 56 | status = LOW; 57 | digitalWrite(pin, status); 58 | delayMicroseconds(out); 59 | } else { 60 | status = HIGH; 61 | if (carrier) { 62 | // Loop, once per pulse. 63 | for (k = 0; k < out / dcTotal; k++) { 64 | digitalWrite(pin, HIGH); 65 | delayMicroseconds(dcHigh); 66 | digitalWrite(pin, LOW); 67 | delayMicroseconds(dcLow); 68 | } 69 | } else { 70 | digitalWrite(pin, status); 71 | delayMicroseconds(out); 72 | } 73 | } 74 | } 75 | 76 | // Reset, just in case. 77 | digitalWrite(pin, LOW); 78 | delayMicroseconds(intraDelay); 79 | yield(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/transmit.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" void ICACHE_RAM_ATTR transmit(char pin, unsigned char message[], unsigned char repeat, unsigned int intraDelay, unsigned char dcHigh, unsigned char dcLow); 4 | --------------------------------------------------------------------------------