// SPDX-FileCopyrightText: 2018 Dave Astels for Adafruit Industries // // SPDX-License-Identifier: MIT /********************************************************************* 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 "arduino_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"); DynamicJsonDocument 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; } auto error = deserializeJson(jsonBuffer, client); client.stop(); if (error) { logln("JSON PARSE ERROR"); display.println("JSON PARSE ERROR"); return NULL; } return strdup(jsonBuffer["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; } auto error = deserializeJson(jsonBuffer, client); client.stop(); if (error) { logln("JSON PARSE ERROR"); display.println("JSON PARSE ERROR"); return NULL; } long start_of_day = jsonBuffer["daily"]["data"][0]["time"]; long raw_sunrise_time = jsonBuffer["daily"]["data"][0]["sunriseTime"]; long raw_sunset_time = jsonBuffer["daily"]["data"][0]["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; } auto error = deserializeJson(jsonBuffer, client); client.stop(); if (error) { logln("JSON PARSE ERROR"); display.println("JSON PARSE ERROR"); return NULL; } JsonArray lights = jsonBuffer["lights"]; uint8_t *light_numbers = (uint8_t*)malloc(lights.size() + 1); light_numbers[0] = (uint8_t)lights.size(); for (uint16_t 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); }