From 2723332e68dc0570a65b82be00f69625ebb1a9b4 Mon Sep 17 00:00:00 2001 From: Dave Astels Date: Fri, 9 Feb 2018 21:48:06 -0500 Subject: [PATCH] Add code for dastels' Hue Controller guide --- .gitignore | 1 + Hue_Controller/Hue_Controller.ino | 511 ++++++++++++++++++++++++++++++ Hue_Controller/LICENSE | 21 ++ Hue_Controller/README.md | 16 + Hue_Controller/setup.sh | 11 + 5 files changed, 560 insertions(+) create mode 100644 Hue_Controller/Hue_Controller.ino create mode 100644 Hue_Controller/LICENSE create mode 100644 Hue_Controller/README.md create mode 100644 Hue_Controller/setup.sh diff --git a/.gitignore b/.gitignore index b25c15b81..2c49db743 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *~ +Hue_Controller/secrets.h diff --git a/Hue_Controller/Hue_Controller.ino b/Hue_Controller/Hue_Controller.ino new file mode 100644 index 000000000..27ebf5724 --- /dev/null +++ b/Hue_Controller/Hue_Controller.ino @@ -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 +#include +#include +#include +#include +#include +#include +#include +#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); +} diff --git a/Hue_Controller/LICENSE b/Hue_Controller/LICENSE new file mode 100644 index 000000000..87439c502 --- /dev/null +++ b/Hue_Controller/LICENSE @@ -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. diff --git a/Hue_Controller/README.md b/Hue_Controller/README.md new file mode 100644 index 000000000..c4f440cf7 --- /dev/null +++ b/Hue_Controller/README.md @@ -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 | diff --git a/Hue_Controller/setup.sh b/Hue_Controller/setup.sh new file mode 100644 index 000000000..e0b077e94 --- /dev/null +++ b/Hue_Controller/setup.sh @@ -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 +