Add code for dastels' Hue Controller guide
This commit is contained in:
parent
de81dd32a0
commit
2723332e68
5 changed files with 560 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
*~
|
||||
Hue_Controller/secrets.h
|
||||
|
|
|
|||
511
Hue_Controller/Hue_Controller.ino
Normal file
511
Hue_Controller/Hue_Controller.ino
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
/*********************************************************************
|
||||
Written by Dave Astels.
|
||||
MIT license, check LICENSE for more information
|
||||
All text above must be included in any redistribution
|
||||
*********************************************************************/
|
||||
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
#include <RTClib.h>
|
||||
#include <WiFi101.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include "Adafruit_MQTT.h"
|
||||
#include "Adafruit_MQTT_Client.h"
|
||||
|
||||
#include "secrets.h"
|
||||
|
||||
#define BUTTON_A 9
|
||||
#define BUTTON_B 6
|
||||
#define BUTTON_C 5
|
||||
#define MOTION_PIN 10
|
||||
#define LIGHT_PIN A1
|
||||
#define NEOPIXEL_PIN A5
|
||||
|
||||
#define ROOM_ID "4"
|
||||
|
||||
#define OLED_RESET 4
|
||||
Adafruit_SSD1306 display(OLED_RESET);
|
||||
|
||||
Adafruit_NeoPixel pixel = Adafruit_NeoPixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
|
||||
|
||||
RTC_DS3231 rtc;
|
||||
|
||||
WiFiClient client;
|
||||
|
||||
// Setup MQTT
|
||||
#define AIO_SERVER "io.adafruit.com"
|
||||
#define AIO_SERVERPORT 1883
|
||||
|
||||
Adafruit_MQTT_Client mqtt(&client, AIO_SERVER, AIO_SERVERPORT, AIO_USER, AIO_KEY);
|
||||
Adafruit_MQTT_Publish photocell_feed(&mqtt, AIO_USER "/feeds/hue-controller.hue-photocell");
|
||||
Adafruit_MQTT_Publish motion_feed(&mqtt, AIO_USER "/feeds/hue-controller.hue-motion");
|
||||
Adafruit_MQTT_Publish control_feed(&mqtt, AIO_USER "/feeds/hue-controller.hue-control");
|
||||
|
||||
DynamicJsonBuffer jsonBuffer(8500);
|
||||
|
||||
const char *hue_ip = NULL;
|
||||
uint8_t *light_numbers = NULL;
|
||||
boolean last_motion = false;
|
||||
|
||||
DateTime *sunrise = NULL;
|
||||
DateTime *sunset = NULL;
|
||||
|
||||
// hardcoded day start/end times
|
||||
|
||||
DateTime wakeup = DateTime(0, 0, 0, 8, 0, 0);
|
||||
DateTime bedtime = DateTime(0, 0, 0, 23, 30, 0);
|
||||
|
||||
boolean need_sunrise_sunset_times = false;
|
||||
|
||||
//#define TRACE 1
|
||||
|
||||
void init_log()
|
||||
{
|
||||
#ifdef TRACE
|
||||
Serial.begin(9600);
|
||||
while (!Serial) {}
|
||||
Serial.println("Starting");
|
||||
#endif
|
||||
}
|
||||
|
||||
void log(const char *msg)
|
||||
{
|
||||
#ifdef TRACE
|
||||
Serial.print(msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
void log(const int i)
|
||||
{
|
||||
#ifdef TRACE
|
||||
Serial.print(i);
|
||||
#endif
|
||||
}
|
||||
|
||||
void logln(const char *msg)
|
||||
{
|
||||
#ifdef TRACE
|
||||
Serial.println(msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
const char *fetch_hue_ip() {
|
||||
logln("Getting HUE IP");
|
||||
display.println("Getting HUE IP");
|
||||
if (!client.connectSSL("www.meethue.com", 443)) {
|
||||
logln("COULD NOT CONNECT");
|
||||
display.println("COULD NOT CONNECT");
|
||||
return NULL;
|
||||
}
|
||||
client.println("GET /api/nupnp HTTP/1.1");
|
||||
client.println("Host: www.meethue.com");
|
||||
client.println("Connection: close");
|
||||
if (!client.println()) {
|
||||
client.stop();
|
||||
logln("CONNECTION ERROR");
|
||||
display.println("CONNECTION ERROR");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char status[32] = {0};
|
||||
client.readBytesUntil('\r', status, sizeof(status));
|
||||
if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
|
||||
client.stop();
|
||||
logln(status);
|
||||
display.println(status);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char endOfHeaders[] = "\r\n\r\n";
|
||||
if (!client.find(endOfHeaders)) {
|
||||
client.stop();
|
||||
display.println("Getting HUE IP");
|
||||
logln("HEADER ERROR");
|
||||
display.println("HEADER ERROR");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JsonArray& root = jsonBuffer.parseArray(client);
|
||||
client.stop();
|
||||
|
||||
if (!root.success()) {
|
||||
logln("JSON PARSE ERROR");
|
||||
display.println("JSON PARSE ERROR");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return strdup(root[0][F("internalipaddress")]);
|
||||
}
|
||||
|
||||
|
||||
boolean fetch_sunrise_sunset(long *sunrise, long *sunset)
|
||||
{
|
||||
logln("Contacting DarkSky");
|
||||
display.println("Contacting DarkSky");
|
||||
if (!client.connectSSL("api.darksky.net", 443)) {
|
||||
logln("COULD NOT CONNECT");
|
||||
display.println("COULD NOT CONNECT");
|
||||
return false;
|
||||
}
|
||||
|
||||
client.print("GET /forecast/");
|
||||
client.print(DARKSKY_KEY);
|
||||
client.print("/42.9837,-81.2497?units=ca&exclude=currently,minutely,hourly,alerts,flags&language=en");
|
||||
client.println(" HTTP/1.1");
|
||||
|
||||
client.print("Host: ");
|
||||
client.println("api.darksky.net");
|
||||
client.println("Connection: close");
|
||||
if (!client.println()) {
|
||||
client.stop();
|
||||
logln("CONNECTION ERROR");
|
||||
display.println("CONNECTION ERROR");
|
||||
return false;
|
||||
}
|
||||
|
||||
char status[32] = {0};
|
||||
client.readBytesUntil('\r', status, sizeof(status));
|
||||
if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
|
||||
client.stop();
|
||||
display.println(status);
|
||||
return false;
|
||||
}
|
||||
|
||||
char endOfHeaders[] = "\r\n\r\n";
|
||||
if (!client.find(endOfHeaders)) {
|
||||
client.stop();
|
||||
logln("HEADER ERROR");
|
||||
display.println("HEADER ERROR");
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonObject& root = jsonBuffer.parseObject(client);
|
||||
client.stop();
|
||||
|
||||
if (!root.success()) {
|
||||
logln("JSON PARSE ERROR");
|
||||
display.println("JSON PARSE ERROR");
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonObject& data = root["daily"]["data"][0];
|
||||
long start_of_day = data["time"];
|
||||
long raw_sunrise_time = data["sunriseTime"];
|
||||
long raw_sunset_time = data["sunsetTime"];
|
||||
|
||||
*sunrise = raw_sunrise_time - start_of_day;
|
||||
*sunset = raw_sunset_time - start_of_day;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
boolean update_sunrise_sunset()
|
||||
{
|
||||
long sunrise_seconds, sunset_seconds;
|
||||
if (!fetch_sunrise_sunset(&sunrise_seconds, &sunset_seconds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sunrise) {
|
||||
delete sunrise;
|
||||
}
|
||||
sunrise = new DateTime(0, 0, 0, sunrise_seconds / 3600, (sunrise_seconds / 60) % 60, 0);
|
||||
|
||||
if (sunset) {
|
||||
delete sunset;
|
||||
}
|
||||
sunset = new DateTime(0, 0, 0, sunset_seconds / 3600, (sunset_seconds / 60) % 60, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t *lights_for_group(const char *group_number)
|
||||
{
|
||||
logln("Finding lights");
|
||||
display.println("Finding lights");
|
||||
if (!client.connect(hue_ip, 80)) {
|
||||
display.println("COULD NOT CONNECT");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
client.print("GET /api/");
|
||||
client.print(HUE_USER);
|
||||
client.print("/groups/");
|
||||
client.print(group_number);
|
||||
client.println(" HTTP/1.1");
|
||||
|
||||
client.print("Host: ");
|
||||
client.println(hue_ip);
|
||||
client.println("Connection: close");
|
||||
if (!client.println()) {
|
||||
client.stop();
|
||||
display.println("CONNECTION ERROR");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char status[32] = {0};
|
||||
client.readBytesUntil('\r', status, sizeof(status));
|
||||
if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
|
||||
client.stop();
|
||||
display.println(status);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char endOfHeaders[] = "\r\n\r\n";
|
||||
if (!client.find(endOfHeaders)) {
|
||||
client.stop();
|
||||
display.println("HEADER ERROR");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JsonObject& group = jsonBuffer.parseObject(client);
|
||||
client.stop();
|
||||
|
||||
if (!group.success()) {
|
||||
display.println("JSON PARSE ERROR");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JsonArray& lights = group["lights"];
|
||||
uint8_t *light_numbers = (uint8_t*)malloc(lights.size() + 1);
|
||||
light_numbers[0] = (uint8_t)lights.size();
|
||||
for (uint i = 0; i < lights.size(); i++) {
|
||||
light_numbers[i+1] = (uint8_t)atoi((const char *)lights[i]);
|
||||
}
|
||||
return light_numbers;
|
||||
}
|
||||
|
||||
|
||||
void update_light(uint8_t light_number, boolean on_off, uint8_t brightness)
|
||||
{
|
||||
if (!client.connect(hue_ip, 80)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log("Turning light ");
|
||||
log(light_number);
|
||||
logln(on_off ? " on" : " off");
|
||||
|
||||
log("PUT /api/");
|
||||
log(HUE_USER);
|
||||
log("/lights/");
|
||||
log(light_number);
|
||||
logln("/state HTTP/1.1");
|
||||
|
||||
char content[32];
|
||||
sprintf(content, "{\"on\":%s,\"bri\":%d}", on_off ? "true" : "false", brightness);
|
||||
|
||||
client.print("PUT /api/");
|
||||
client.print(HUE_USER);
|
||||
client.print("/lights/");
|
||||
client.print(light_number);
|
||||
client.println("/state HTTP/1.1");
|
||||
|
||||
client.print("Host: ");
|
||||
client.println(hue_ip);
|
||||
|
||||
client.println("Connection: close");
|
||||
|
||||
client.print("Content-Type: ");
|
||||
client.println("application/json");
|
||||
client.println("User-Agent: FeatherM0Sender");
|
||||
client.print("Content-Length: ");
|
||||
client.println(strlen(content));
|
||||
client.println();
|
||||
|
||||
client.println(content);
|
||||
client.stop();
|
||||
}
|
||||
|
||||
|
||||
void update_all_lights(uint8_t *light_numbers, boolean on_off, uint8_t brightness)
|
||||
{
|
||||
if (light_numbers != NULL) {
|
||||
uint8_t num_lights = light_numbers[0];
|
||||
for (int i = 0; i < num_lights; i++) {
|
||||
update_light(light_numbers[i+1], on_off, brightness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
boolean is_between(DateTime *now, DateTime *start, DateTime *end)
|
||||
{
|
||||
// now > start || now > end
|
||||
if (now->hour() < start->hour()) return false;
|
||||
if (now->hour() == start->hour() && now->minute() < start->minute()) return false;
|
||||
if (now->hour() > end->hour()) return false;
|
||||
if (now->hour() == end->hour() && now->minute() > end->minute()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void MQTT_connect()
|
||||
{
|
||||
if (mqtt.connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (mqtt.connect() != 0) { // connect will return 0 for connected
|
||||
mqtt.disconnect();
|
||||
delay(5000); // wait 5 seconds
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void setup()
|
||||
{
|
||||
init_log();
|
||||
|
||||
pinMode(BUTTON_A, INPUT_PULLUP);
|
||||
pinMode(BUTTON_B, INPUT_PULLUP);
|
||||
pinMode(BUTTON_C, INPUT_PULLUP);
|
||||
pinMode(MOTION_PIN, INPUT_PULLUP);
|
||||
|
||||
|
||||
display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // initialize with the I2C addr 0x3C (for the 128x32)
|
||||
display.display();
|
||||
delay(2000);
|
||||
display.setTextSize(1);
|
||||
display.setTextColor(WHITE);
|
||||
|
||||
WiFi.setPins(8,7,4,2);
|
||||
|
||||
// // attempt to connect to WiFi network:
|
||||
int status = WL_IDLE_STATUS;
|
||||
|
||||
display.print(WIFI_SSID);
|
||||
while (status != WL_CONNECTED) {
|
||||
display.print(".");
|
||||
delay(500);
|
||||
status = WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
}
|
||||
|
||||
if (! rtc.begin()) {
|
||||
while (1);
|
||||
}
|
||||
|
||||
if (rtc.lostPower()) {
|
||||
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
|
||||
}
|
||||
|
||||
// Clear the buffer.
|
||||
display.clearDisplay();
|
||||
display.setCursor(0, 0);
|
||||
|
||||
hue_ip = fetch_hue_ip();
|
||||
if (hue_ip == NULL) {
|
||||
while (true) {
|
||||
}
|
||||
}
|
||||
light_numbers = lights_for_group(ROOM_ID);
|
||||
if (light_numbers == NULL) {
|
||||
while (true) {
|
||||
}
|
||||
}
|
||||
|
||||
pixel.begin();
|
||||
pixel.setPixelColor(0, 0, 0, 0);
|
||||
pixel.show();
|
||||
|
||||
if (!update_sunrise_sunset()) {
|
||||
sunrise = new DateTime(0, 0, 0, 7, 0, 0);
|
||||
sunset = new DateTime(0, 0, 0, 16, 30, 0);
|
||||
}
|
||||
}
|
||||
|
||||
long ping_time = 0;
|
||||
void ping_if_time(DateTime now)
|
||||
{
|
||||
if (now.secondstime() >= ping_time) {
|
||||
ping_time = now.secondstime() + 250;
|
||||
if (!mqtt.ping()) {
|
||||
logln("No MQTT ping");
|
||||
mqtt.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void loop()
|
||||
{
|
||||
display.clearDisplay();
|
||||
display.setCursor(0, 0);
|
||||
|
||||
DateTime now = rtc.now();
|
||||
ping_if_time(now);
|
||||
MQTT_connect();
|
||||
|
||||
|
||||
boolean is_motion = digitalRead(MOTION_PIN);
|
||||
int32_t light_level = analogRead(LIGHT_PIN);
|
||||
char buf[22];
|
||||
sprintf(buf, "%d/%02d/%02d %02d:%02d:%02d", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
|
||||
|
||||
if (now.hour() == 0) {
|
||||
if (need_sunrise_sunset_times) {
|
||||
if (!update_sunrise_sunset()) {
|
||||
while (true) {
|
||||
}
|
||||
}
|
||||
need_sunrise_sunset_times = false;
|
||||
}
|
||||
} else {
|
||||
need_sunrise_sunset_times = true;
|
||||
}
|
||||
|
||||
boolean motion_started = is_motion && !last_motion;
|
||||
boolean motion_ended = !is_motion && last_motion;
|
||||
last_motion = is_motion;
|
||||
|
||||
if (motion_started) {
|
||||
logln("Publishing motion start");
|
||||
if (!motion_feed.publish((const char*)"started")) {
|
||||
logln("\n***** MQTT motion publish failed\n");
|
||||
}
|
||||
if (!photocell_feed.publish(light_level)) {
|
||||
logln("\n***** MQTT photocell publish failed\n");
|
||||
}
|
||||
pixel.setPixelColor(0, 16, 0, 0);
|
||||
} else if (motion_ended) {
|
||||
if (!motion_feed.publish((const char*)"ended")) {
|
||||
logln("\n***** MQTT motion publish failed\n");
|
||||
}
|
||||
pixel.setPixelColor(0, 0, 0, 0);
|
||||
}
|
||||
pixel.show();
|
||||
|
||||
display.println(buf);
|
||||
if (is_motion) {
|
||||
display.print(" ");
|
||||
} else {
|
||||
display.print("No");
|
||||
}
|
||||
display.print(" Motion Light: ");
|
||||
display.println(light_level);
|
||||
display.print(is_between(&now, sunrise, sunset) ? "Light" : "Dark");
|
||||
display.println(" out now");
|
||||
display.print("You should be ");
|
||||
display.println(is_between(&now, &wakeup, &bedtime) ? "awake" : "asleep");
|
||||
display.display();
|
||||
|
||||
if (!digitalRead(BUTTON_A) || (motion_started && (light_level < 50 || !is_between(&now, sunrise, sunset)))) {
|
||||
if (!control_feed.publish((const char*)"on")) {
|
||||
logln("\n***** MQTT control publish failed\n");
|
||||
}
|
||||
update_all_lights(light_numbers, true, is_between(&now, &wakeup, &bedtime) ? 100 : 10);
|
||||
}
|
||||
if (!digitalRead(BUTTON_C) || motion_ended) {
|
||||
if (!control_feed.publish((const char*)"off")) {
|
||||
logln("\n*****MQTT control publish failed\n");
|
||||
}
|
||||
update_all_lights(light_numbers, false, 0);
|
||||
}
|
||||
|
||||
delay(400);
|
||||
}
|
||||
21
Hue_Controller/LICENSE
Normal file
21
Hue_Controller/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Dave Astels
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
16
Hue_Controller/README.md
Normal file
16
Hue_Controller/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# hue_controller
|
||||
|
||||
Firmware for motion/light/time driven hue lighting controller.
|
||||
|
||||
Use with the Arduino IDE, with FeatherM0 selected as the board.
|
||||
|
||||
Run the setup.sh script first to build secrets.h.
|
||||
|
||||
| Env Var | meaning |
|
||||
| ----------- | --------------------------------------:|
|
||||
| WIFI_SSID | The SSID of your Wifi |
|
||||
| WIFI_PASS | the password for your Wifi |
|
||||
| HUE_USER | Your user id for the HUE developer API |
|
||||
| DARKSKY_KEY | Your user id for the darksky.net API |
|
||||
| AIO_USER | Your Adafruit IO username |
|
||||
| AIO_KEY | Your Adafruit IO secret key |
|
||||
11
Hue_Controller/setup.sh
Normal file
11
Hue_Controller/setup.sh
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Set any env vars needed to build
|
||||
|
||||
rm secrets.h
|
||||
echo "#define WIFI_PASS \"$WIFI_PASSWORD\"" >> secrets.h
|
||||
echo "#define WIFI_SSID \"$WIFI_SSID\"" >> secrets.h
|
||||
echo "#define HUE_USER \"$HUE_USER\"" >> secrets.h
|
||||
echo "#define DARKSKY_KEY \"$DARKSKY_KEY\"" >> secrets.h
|
||||
echo "#define AIO_USER \"$AIO_USER\"" >> secrets.h
|
||||
echo "#define AIO_KEY \"$AIO_KEY\"" >> secrets.h
|
||||
echo "" >> secrets.h
|
||||
|
||||
Loading…
Reference in a new issue