Add code for dastels' Hue Controller guide

This commit is contained in:
Dave Astels 2018-02-09 21:48:06 -05:00
parent de81dd32a0
commit 2723332e68
5 changed files with 560 additions and 0 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
*~
Hue_Controller/secrets.h

View 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
View 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
View 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
View 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