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.setCursor(0, 0);
  DateTime now =;

  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.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"));
    pixel.setPixelColor(0, 16, 0, 0);
  } else if (motion_ended) {
    motion_feed.publish((const char*)"ended"));
    pixel.setPixelColor(0, 0, 0, 0);
  if (is_motion) {
    display.print("  ");
  } else {
  display.print(" Motion  Light: ");
  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");

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

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.


This guide was first published on Feb 14, 2018. It was last updated on Jul 14, 2024.

This page (Putting it all together) was last updated on Mar 08, 2024.

Text editor powered by tinymce.