More Clever Night Light in Micropython
FleetingTable of Contents
more clever night light using a Adafruit Feather HUZZAH with ESP8266 with micropython
Trying to avoid the mess of dealing with android version discrepancies of more clever night light in kivy, and trying to have a much faster crash recovery than in more clever night light.
The logic is handled by a server in my stack that the device requests to find out how much time to sleep and what color to set. That way, most of the fix and features will go to the server and I will barely have to wake up the device to change its code.
on the server
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import os
from datetime import datetime, timedelta
from flask import Flask
app = Flask(__name__)
debug = os.environ.get("DEBUG") == "true"
school_schedule = {
"almost_hour": 6,
"almost_minute": 30,
"getup_hour": 7,
"getup_minute": 0,
"day_hour": 7,
"day_minute": 40,
"evening_hour": 20,
"evening_minute": 0,
"night_hour": 20,
"night_minute": 30,
}
holiday_schedule = {
"almost_hour": 7,
"almost_minute": 30,
"getup_hour": 8,
"getup_minute": 0,
"day_hour": 8,
"day_minute": 40,
"evening_hour": 20,
"evening_minute": 0,
"night_hour": 20,
"night_minute": 30,
}
debug_schedule = []
orange = (50, 25, 0)
red = (30, 0, 0)
black = (0, 0, 0)
green = (0, 30, 0)
def times():
now = datetime.now()
if now.weekday() in [0, 1, 3, 4]: # mon, tue, thu, fri
schedule = school_schedule
else:
schedule = holiday_schedule
# but, if past the night of today's schedule, use the other values from the
# other schedule, keeping the night of this schedule to avoid issues like when
# they are different
night = now.replace(
hour=schedule["night_hour"],
minute=schedule["night_minute"],
second=0,
)
if now >= night:
if now.weekday() in [6, 2]: # sun, wed, change holidays -> school
# change of schedule
schedule = school_schedule
elif now.weekday() in [1, 4]: # tue, fri, change school -> holidays
# change of schedule
schedule = holiday_schedule
almost = now.replace(
hour=schedule["almost_hour"],
minute=schedule["almost_minute"],
second=0,
)
getup = now.replace(
hour=schedule["getup_hour"],
minute=schedule["getup_minute"],
second=0,
)
day = now.replace(
hour=schedule["day_hour"],
minute=schedule["day_minute"],
second=0,
)
evening = now.replace(
hour=schedule["evening_hour"],
minute=schedule["evening_minute"],
second=0,
)
# don't recompute night for we are sure to use the schedule of today for it
res = now, almost, getup, day, evening, night
if debug:
if not debug_schedule or debug_schedule[-1] < now:
debug_schedule.clear()
debug_schedule.extend(
[(now + timedelta(minutes=(i + 1))).replace(second=0) for i in range(5)]
)
print(debug_schedule)
return [now] + debug_schedule
return res
@app.route("/")
def next():
try:
return _next()
except:
raise
def _next():
now, almost, getup, day, evening, night = times()
if now < almost:
# night
return step(
next_time="almost",
upcolornow=black,
bottomcolornow=black,
upcolornext=black,
bottomcolornext=orange,
)
elif now < getup:
# almost
return step(
next_time="getup",
upcolornow=black,
bottomcolornow=orange,
upcolornext=green,
bottomcolornext=black,
)
elif now < day:
# getup
return step(
next_time="day",
upcolornow=green,
bottomcolornow=black,
upcolornext=black,
bottomcolornext=black,
)
elif now < evening:
# day
return step(
next_time="evening",
upcolornow=black,
bottomcolornow=black,
upcolornext=black,
bottomcolornext=red,
)
elif now < night:
# evening
return step(
next_time="night",
upcolornow=black,
bottomcolornow=red,
upcolornext=black,
bottomcolornext=black,
)
else:
# night
return step(
next_time="almost",
upcolornow=black,
bottomcolornow=black,
upcolornext=black,
bottomcolornext=orange,
)
def step(next_time, upcolornow, upcolornext, bottomcolornow, bottomcolornext):
now, almost, getup, day, evening, night = times()
if next_time == "almost":
next_time = almost
if next_time == "getup":
next_time = getup
if next_time == "day":
next_time = day
if next_time == "evening":
next_time = evening
if next_time == "night":
next_time = night
duration = (next_time - now).total_seconds()
if duration < 0:
duration = duration + (24 * 3600)
if duration > (20 if debug else 200):
command = {
"sleep": duration - (10 if debug else 100),
"upcolor": upcolornow,
"bottomcolor": bottomcolornow,
}
else:
command = {
"in": duration,
"upcolor": upcolornext,
"bottomcolor": bottomcolornext,
}
print(f"{now}: {command}", flush=True)
return command
@app.route("/health")
def health():
return "ok", 200
on the MCU
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import gc
import sys
import time
import os
import esp
import machine
import neopixel
import network
import ntptime
import requests
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
upled = neopixel.NeoPixel(machine.Pin(4), 1)
bottomled = neopixel.NeoPixel(machine.Pin(0), 1)
def format_message(msg):
# _, _, _, h, m, _, _, _ = time.localtime()
# return f"{h:02d}:{m:02d}: {msg}"
return f"{msg}"
def say(message):
print(format_message(message))
def notify(message):
say(message)
try:
requests.post("http://home:9705/nightlight",
data=f"{network.hostname()}: {format_message(message)}")
except Exception as e:
say(f"Could not notify with message: {message}, {e}")
def wlan_connect():
wlan.connect()
for i in range(30):
if wlan.isconnected():
break
else:
say("waiting 1s to be connected")
time.sleep(1)
if i > 9:
notify(f"connected after {i} iterations")
say("Hello")
for led in upled, bottomled:
led.fill([5, 0, 5])
led.write()
led.write()
wlan_connect()
for led in upled, bottomled:
led.fill([0, 0, 3])
led.write()
led.write()
# * SLEEP_LIGHT - light sleep, shuts down the WiFi Modem circuit and suspends the processor periodically.
# The system enters the set sleep mode automatically when possible.
try:
esp.sleep_type(esp.SLEEP_LIGHT)
except:
notify(f"Could not set the sleep light type on {sys.platform}")
if "nowait" not in os.listdir():
notify("Waiting a bit before starting")
time.sleep(10)
notify("Here we go")
for led in upled, bottomled:
led.fill([0, 0, 0])
led.write()
led.write()
def setcolors(command):
upcolor = command["upcolor"]
bottomcolor = command["bottomcolor"]
say(f"Setting up:{upcolor}, bottom:{bottomcolor}")
upled.fill(upcolor)
upled.write()
upled.write()
bottomled.fill(bottomcolor)
bottomled.write()
bottomled.write()
while True:
try:
wlan_connect()
command = requests.get("http://home:9911").json()
notify(f"Got: {command}")
if "sleep" in command:
setcolors(command)
if "keepconnected" not in os.listdir():
wlan.disconnect()
# we cannot sleep longer than 2**32 / 1000 / 3600 hours ~ 1 hour due
# to the fact internally micropython uses us and the processor is
# 32bits. Therefore we may want to split this into several sleep
# commands.
time_to_wait = int(command["sleep"])
while time_to_wait > 0:
duration = min(time_to_wait, 3600)
time_to_wait = time_to_wait - duration
sleep_time = duration * 1000
say(f"Now, sleeping for {sleep_time}ms, still {time_to_wait}s to go after")
machine.lightsleep(sleep_time)
elif "in" in command:
say(f"Waiting for {int(command['in'])}")
time.sleep(int(command["in"]))
setcolors(command)
say("wait a bit before asking again, but be precise. It won't take long")
time.sleep(3) # if debug else 300)
else:
raise NotImplementedError()
except Exception as e:
notify(f"Failed: {e}, waiting for 5m before trying again")
time.sleep(300)
spawn webrepl_cli.py -p 0000 nightlight
op:repl, host:nightlight, port:8266, passwd:0000.
Remote WebREPL version: (1, 25, 0)
Use Ctrl-] to exit this shell
00:00: Here we go
Traceback (most recent call last):
File "main.py", line 64, in <module>
File "main.py", line 32, in notify
File "requests/__init__.py", line 209, in post
File "requests/__init__.py", line 81, in request
KeyboardInterrupt:
MicroPython v1.25.0 on 2025-04-15; ESP module with ESP8266
Type "help()" for more information.
>>>
trying it
So far, so good. I eventually moved the code to a wemos d1. This had several advantages:
- it has a 5V output, that is required by the neopixel. I even put 2 of those in the final design,
- I could keep the huzzah for more complicated scenarios, involving feathers,
- this was an opportunity to test how fast I could recover from a crash of device. It was only a 600K download (the micropython firmware) and a couple of minutes to setup. This was much more practical than the initial version.
- I could easily use another device (esp32c3), put the code into it to try updates before sending them to the device “in production”.
Now, only time will tell if the device will manage to deal with.




