Overview

The Philips HUE lighting system is very cool. Wifi control of your lighting, full color lighting, etc. You simply replace your regular bulbs with HUE bulbs and gain the an immediate improvement by having switched to LED lighting. Open up the HUE app on your smartphone and you can have very precise control of on/off, brightness, and color (with color bulbs) of individual bulbs.

That's great, but is having to dig out your phone is as bad as having to walk to the wall switch, other than not have to get out of bed to turn lights on/off. And who carries their phone when going for a midnight snack? Using an voice assistant like Amazon Echo (which I use) helps, but you still have to proactively do something and you have to be within earshot of the device. Wouldn't it be nice if the lighting was controlled for you, turning on and off automatically.  That's what we'll do in the project.

Parts

Formats

For this project, I decided to use boards in the Feather ecosystem.  Because it was going to need WiFi connectivity interact with the HUE system, I chose the Feather M0 WiFi.  It also needed to know what time it was (we'll see why later) so I chose the DS3231 RTC featherwing. To display some status output I added an OLED featherwing. I make use of the A and C buttons on the OLED wing to provide a manual override of lighting for testing. These can stack up (with the OLED and sensor wings on top!) with the right headers and a FeatherWing Doubler or Tripler. I used the tripler in my build since I had one sitting around unused.

To round it out I built a custom feather wing with the sensors I needed and a NeoPixel for a status indicator.

Adafruit Feather M0 WiFi - ATSAMD21 + ATWINC1500

PRODUCT ID: 3010
Feather is the new development board from Adafruit, and like its namesake it is thin, light, and lets you fly! We designed Feather to be a new standard for portable microcontroller...
$34.95
IN STOCK

DS3231 Precision RTC FeatherWing - RTC Add-on For Feather Boards

PRODUCT ID: 3028
A Feather board without ambition is a Feather board without FeatherWings! This is the DS3231 Precision RTC FeatherWing: it adds an extremely accurate I2C-integrated...
$13.95
IN STOCK

FeatherWing OLED - 128x32 OLED Add-on For All Feather Boards

PRODUCT ID: 2900
A Feather board without ambition is a Feather board without FeatherWings! This is the FeatherWing OLED: it adds a 128x32 monochrome OLED plus 3 user buttons to...
$14.95
IN STOCK

Photo cell (CdS photoresistor)

PRODUCT ID: 161
CdS cells are little light sensors. As the squiggly face is exposed to more light, the resistance goes down. When its light, the resistance is about 5-10KΩ, when dark it goes up...
$0.95
IN STOCK

PIR (motion) sensor

PRODUCT ID: 189
PIR sensors are used to detect motion from pets/humanoids from about 20 feet away (possibly works on zombies, not guaranteed). This one has an adjustable delay before firing (approx...
$9.95
IN STOCK

Breadboard-friendly RGB Smart NeoPixel - Pack of 4

PRODUCT ID: 1312
This is the easiest way possible to add small, bright RGB pixels to your project. We took the same technology from our Flora NeoPixels and made them breadboard friendly, with two rows...
$7.95
IN STOCK

FeatherWing Proto - Prototyping Add-on For All Feather Boards

PRODUCT ID: 2884
A Feather board without ambition is a Feather board without FeatherWings!This is the FeatherWing Proto - a prototyping add-on for all Feather boards. Using our...
$4.95
IN STOCK

FeatherWing Tripler Mini Kit - Prototyping Add-on For Feathers

PRODUCT ID: 3417
This is the FeatherWing Tripler - a prototyping add-on and more for all Feather boards. This is similar to our
$8.50
IN STOCK

Custom Sensor Wing

The off the shelf Feather/Wings provide the computing power, wifi,  timekeeping, and status display (which isn't really needed but is nice while working on the software). What's missing is inputs to the system. Specifically knowing when someone is in the room and how light/dark it is in the room.  I use this combined with the time of day to figure out how to control the lights.

For motion sensing I use a basic PIR. For light measurement I use a simple CdS photocell in a voltage divider.

There are already excellent learning guides on photoresisters and PIRs so I won't redo them here.

Notice that I'm also using a NeoPixel. This I'll use as an at a glance status indicator since the wifi Feather M0 doesn't have a Dotstar. Even if it did, the Dotstar could easily be covered depending on how the boards are stacked.

As usual, I started with the lower profile components: the NeoPixel breakout and the photocell voltage divider. Keep in mind that the photocell has to be clear of the PIR and able to "see" the ambient light.

I replaced the header pins on the PIR with longer ones that would reach past the capacitors and into the holes on the proto wing.

The PIR fits neatly between the photoresistor and neopixel.

For this one-off unit I did simple point to point wiring, largely on the underside of the board.

There are several functional areas in the code, and I'll go through each individually.

The code here has been edited for clarify.  The actual code in the repository has more error checking and diagnostic output.

You can find the full code listing here!

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

Talking to HUE

Finding your HUE Bridge

I'm using the Wifi101 library to handle communication and ArduinoJson library to handle json generation and parsing.

Be sure thatyour WiFi module's firmware and SSL certificates are up to date. See the Adafruit Feather M0 WiFi with ATWINC1500 tutorial for details.

To use the HUE restful interface to control lighting you first have to find out what to talk to.

const char *fetch_hue_ip() {
  if (!client.connectSSL("www.meethue.com", 443)) {
    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();
    return NULL;
  }

  char status[32] = {0};
  client.readBytesUntil('\r', status, sizeof(status));
  if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
    client.stop();
    return NULL;
  }

  char endOfHeaders[] = "\r\n\r\n";
  if (!client.find(endOfHeaders)) {
    client.stop();
    return NULL;
  }

  JsonArray& root = jsonBuffer.parseArray(client);
  client.stop();

  if (!root.success()) {
    return NULL;
  }

  return strdup(root[0][F("internalipaddress")]);
}

It starts by making a secure connection to www.meethue.com doing a GET from /api/nupnp. If the result has a 200 status the response body is parsed and the internalipaddress member is extracted.  That's the IP on the local network of the HUE bridge device.

To access the API on your HUE bridge device, you'll need a user. See the HUE Getting Started guide for details.

Light groups

In addition to managing individual lights, HUE allows you group lights. The idea behind this is to group lights into rooms to allow you to control all the lights in a room at once from the hue smartphone app.

While everything about your local HUE installation is discoverable through the RESTful API, I've simplified things a bit by hardcoding the group id that the device should control. In a full installation each module would need to have its room/group id set somehow. In my final vision, the name of the room the module is located in would be assigned from a central controller.  That's beyond the scope of this guide, though.

hue_ip = fetch_hue_ip();
if (hue_ip == NULL) {
  while (true) {
  }
}
light_numbers = lights_for_group(ROOM_ID);
if (light_numbers == NULL) {
  while (true) {
  }
}

The above code is from setup(). It gets the IP of the local HUE bridge and then asks for the lights in the room that's been set.

Notice that I use the first (i.e the 0th) element in the array of light numbers to stash the number of lights for later iteration.

uint8_t *lights_for_group(const char *group_number)
{
  if (!client.connect(hue_ip, 80)) {
    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();
    return NULL;
  }

  char status[32] = {0};
  client.readBytesUntil('\r', status, sizeof(status));
  if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
    client.stop();
    return NULL;
  }

  char endOfHeaders[] = "\r\n\r\n";
  if (!client.find(endOfHeaders)) {
    client.stop();
    return NULL;
  }

  JsonObject& group = jsonBuffer.parseObject(client);
  client.stop();

  if (!group.success()) {
    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;
}

Controlling lights

Once the list of lights is in hand, they can be controlled.

void update_light(uint8_t light_number, boolean on_off, uint8_t brightness)
{
  if (!client.connect(hue_ip, 80)) {
    return;
  } 

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

To keep things simple this just fires & forgets, assuming the lights will get set appropriately. You can close the feedback loop to some extent by querying the HUE some time later to verify that the change was made to the lights.

Getting the Weather

Ok, we don't really care about the weather. I use DarkSky to fetch the weather data for the day but ignore everything other than the sunrise and sunset times. Using that information along with the time of day from the RTC, the system can determine if it's day or night.

boolean fetch_sunrise_sunset(long *sunrise, long *sunset)
{
  if (!client.connectSSL("api.darksky.net", 443)) {
    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();
    return false;
  }

  char status[32] = {0};
  client.readBytesUntil('\r', status, sizeof(status));
  if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
    client.stop();
    return false;
  }

  char endOfHeaders[] = "\r\n\r\n";
  if (!client.find(endOfHeaders)) {
    client.stop();
    return false;
  }

  JsonObject& root = jsonBuffer.parseObject(client);
  client.stop();

  if (!root.success()) {
    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;
}

The Darksky api takes url arguments for the location, units, as well as data items to exclude. In this case I have it exclude as much as possible to minimize the size of the response body.

Note that fetch_sunrise_sunset returns times in seconds, be relative to the start of the day. That then gets converted to DateTime objects for use later.

Using Adafruit IO

For fun I decided to log some things to AdafruitIO:

  • motion start/end
  • light level when motion is detected
  • when lights are turned on/off

Feeds are created in the preamble:

// Setup MQTT
#define AIO_SERVER      "io.adafruit.com"
#define AIO_SERVERPORT  1883
const char MQTT_SERVER[] PROGMEM    = AIO_SERVER;
const char MQTT_USERNAME[] PROGMEM  = AIO_USER;
const char MQTT_PASSWORD[] PROGMEM  = AIO_KEY;

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

We make use of those feeds in loop(). For example:

 if (motion_started) {
  motion_feed.publish((const char*)"started"));
  photocell_feed.publish(light_level));
} else if (motion_ended) {
  motion_feed.publish((const char*)"ended"));
}

Keys and such

You should never, ever put keys and other confidential information in code. I have all mine in environment variables and I use a shell script to put them in a header field that gets included. If you use this approach, just be careful to add that header file to .gitignore.

# 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

This will result in a file that looks like:

#define WIFI_PASS   "....................."
#define WIFI_SSID   "....."
#define HUE_USER    "..................................."
#define DARKSKY_KEY "....................................."
#define AIO_USER    "......"
#define AIO_KEY     "......................................."

Putting it all together

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 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) {
    motion_feed.publish((const char*)"started"));
    photocell_feed.publish(light_level));
    pixel.setPixelColor(0, 16, 0, 0);
  } else if (motion_ended) {
    motion_feed.publish((const char*)"ended"));
    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)))) {
    control_feed.publish((const char*)"on"));
    update_all_lights(light_numbers, true, is_between(&now, &wakeup, &bedtime) ? 100 : 10);
  }
  if (!digitalRead(BUTTON_C) || motion_ended) {
    control_feed.publish((const char*)"off"));
    update_all_lights(light_numbers, false, 0);
  }
  
  delay(400);
}

This starts by reading the motion detector and light sensor. Then if we've passed midnight we update the sunrise and sunset times for the new day. A flag is used to note that they've been updated so that it will only update once per day (i.e. the first time the hour is 0. Once the hour passes 0, the flag is reset for the next day.

Next, motion start/end is detected. It looks for a change in the state of the PIR. To do this it tracks the previously observed state. When the new state differs, there's a state change.

If motion just started or ended, it posts to the motion feed on AdafruitIO and changes the color of the NeoPixel appropriately. If motion started, it also posts the light reading to the light level feed.

Relevant information is also displayed on the OLED.

The crux of the process is at the end of loop(). This is the code that decides if the lights should be turned on or off.

Lights are turned on if

  • the on button is pushed, or
  • motion has started and
    • it's rather dark in the room or
    • it's dark outside.

Additionally, if the current time is between the (for now) hardcoded get up and got to bed times the lights are turned on at full brightness, otherwise (i.e. you should be asleep but are up for a snack or drink) they turn on at 10% brightness.

Lights are turned off if

  • the off button is pushed or
  • motion has stopped.

In any other case, the lights are not changed. As you can see, the trigger for changing the lights is the push of one of the control buttons, or a change in motion state.

 

Downloads