/circadian_hue

Continuous automation for Philips Hue to control light cycles through AWS

Primary LanguageTypeScript

Circadian Rhythm in The Cloud

Light and how it affects us

Melanopsin(subfamily of the Opsin proteins) is a photosensitive protein found in mammalian retina. When light contacts the human eyes, melanopsin reacts according to the presence of blue light, with peak light absorption at 480nm. As light absorption of melanopsin is increased, a negative correlative relationship with melatonin is exhibited. In other words: the closer we get to 480nm blue light, the brain releases less melatonin, while a decrease from 480nm increases the release of melatonin in the mammalian brain.

What is this?

This project is an experiment in whether AWS would be a good method by which to control the lights in my office to help with sleep schedule and other things like seasonal depression. This project is meant to be deployed on AWS and is designed as a serverless application (other than an optional data collection pipeline).

Lambda functions are cheap to run so this makes it viable but latency is a potential problem over the span of a day (you'll see a cost and latency analysis below).

The lambda functions are Python (with Docker) and the infrastructure is written using AWS CDK V2 for TypeScript.

How to deploy

  1. Get access to the Hue Bridge by generating a key. Hit the button on the bridge and send the following POST:
https://<bridge ip address>/api

{"devicetype":"<app_name>#<instance_name>", "generateclientkey":true}
  1. Configure your router to port forward from some available port to 443. *The bridge supports HTTPS traffic but might be better to authenticate first using Ngnix.

  2. Some lambda functions employ Docker to install necessary dependencies so make sure to dockerd

  3. in cdk.json the following context variables are set:

Key Function
hueAddress Address of the Bridge to forward control requests, ie <your_ip>:port
hueApiKey Authentication key distributed by the Bridge
lightGroup Identifier representing a group of lights recognized by the bridge
lambdaFunctions Object containing functions to deploy with optional function for data collection

Including "dbWriteFnModuleName":"dbWriterFn" in the lambdaFunction object sets a flag in the main stack to deploy the data collection function and associated components.

  1. Deploy using normal cdk commands, cdk bootstrap, cdk synth, cdk deploy

How temperature values are calculated

Considering the spin of the world traces a circle, we can likely expect that the sun relative to us moves in a sinusoidal fashion. From this, we can expect the greatest slope (change in sun's level) at the start and end of the day. I don't want to exactly mimic this because maybe I'd like to wake up a bit more rapidly than the sun allows, or have a slightly longer day with a more rapid sundown than the current season would allow. I'd like to get somewhat close though. To achieve this I use a quadratic interpolation with a set of values representing time (minutes elapsed in the day) and temperature (in Kelvin). (Polynomial interpolations are cheap and relatively effective, so it's a good choice here)

Using some values that I'd like my day to be based on:

minute_x = [420,  480,  540,  780,  1080, 1200, 1260]
temp_k_y = [2000, 2500, 2857, 5000, 3500, 2500, 2000]

numpy.polynomial.Polynomial.fit(minute_x, temp_k_y, deg=6) gives us a pretty nice curve: drawing

X is the time of day and Y is light temperature. A higher temperature light represents more blue light which generally correlates with the position of the sun.

Red here represent points of the input. We can see that the curve is pretty smooth and with little error (in fact, R^2 = .9940032 so other inputs of a similar shape will do fine). A degree 6 here may seem a little like overkill, but it's the lowest degree where the outlier value will still be hit. This is important as we want daytime light during the daytime.

Architecture

In order to make this work the following happens:

drawing

Core functionality

The yellow region defines core functionality with the following functions:

API Gateway routes request to the functions. Depending on the request the associated function handles the action.

ModelGeneratorFn

Uses Numpy to generate a model that we can call as a functions. This model gets serialized and put into an S3 bucket. The function also updates an environment variable of TemperatureCycleFn to the new object's name. This function can also be called using API Gateway to generate a new model with new values located in the payload.

Because Numpy is not available in the Lambda instance by default, this function is deployed containerized w/ Numpy.

UpdaterFn

Receives API requests to turn the lights on and off. When the lights get turned off, the cron rule gets disabled to avoid needlessly instantiating. When the lights are turned on again, the cron rule is re-enabled. The function also writes the time and action performed to a Postgre table, this will later be used to train a model.

TemperatureCycleFn

is invoked every minute by an EventBridge cron rule. It gets the model object from the bucket, generates the new temperature, and sends a PUT request to the light bulbs on my local network using an API key saved in an SSM Parameter. The request here doesn't matter so the request is invoked on a separate thread and allows us to exit the function before a return is received. TODO: implement error handling and notification

Auxiliary data pipeline

Blue represent an optional data pipeline

The UpdaterFn sends a message containing action and time to an SQS queue. A DB writer function will parse the message and insert into a table on a Postgres RDS instance. The function is invoked by SQS and will pull all available messages from the queue. If the function fails to write messages the message will remain in the queue. If messages older than an hour are read, an SNS notification will be sent out.

For Python to communicate with the Postgres DB, the psycopg library is needed so the function is deployed containerized to install the dependencies as it is not available by default in Lambda's runtime.

Not included in the diagram is an initializing lambda function. This function will create a table in the DB upon deployment (using CustomResource) and then delete itself.

This also deploys a VPC with private and public subnets. The DB instance lives in the private subnet and can be communicated with via a NAT inside the public subnet.

Cost and Timing Analysis

This was written recently and I have yet to test.

Shortcomings

There are several functional shortcomings such as where I'm communicating from. I designed this mainly for my home office but can only talk to the system from there, simple fix should address this.

  • dimming is needed
  • Need to implement error handling
  • The current address is the one AWS provides, should integrate my own
  • API is still incredibly rudimentary
    • should change light trigger to set off locally and only make api call containing the action as payload