Adafruit_Learning_System_Gu.../Hue_Controller/Hue_Controller.ino
2022-02-23 13:54:21 -05:00

516 lines
12 KiB
C++

// 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 <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 "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);
}