The Harvest Light

28 July, 2016

The Situation

For every piece of billable work I do, I use a web-based time-tracking and invoicing system called Harvest. It’s pretty great. I’m forgetful though, and if I don’t start the timer, I can’t invoice for the work. Problem. What I need is a fancy reminder.

The Idea

Use technology to replace my memory. YES. If I had some kind of not-too-distracting visual indication of whether or not a timer is running, I wouldn’t need to remember anything, ever. PARTY.

Alright, so it needs to be something that needs no extra work from me (like… flipping a switch when I start the timer wouldn’t work at all, because I’d end up flipping it before starting the timer, and intending-but-failing to start it… for sure this time…). Harvest has an API, so maybe I can hook into that? And control the light from there? Then the thing will Just Work.

Turns out that the Harvest API can totally do what I need to do! With only minor hoop-jumping! More on which, later.

The Hardware

I’m using an $8 NodeMCU board from Banggood. (This thing is like a tiny Arduino with built-in wifi. It’s basically an ESP8266 with an interface board attached. I don’t know how they are able to make them so cheap.) The Device periodically connects to the Harvest API to check whether or not the Harvest timer is running, then turns on a bunch of LEDs if it is. If the timer’s not running, nothing lights up. The LEDs are loosely arranged to form my logo, because that made me happy and isn’t that the point of life? YES.

To drive the ten LEDs from one low-power output, I used a BC548 NPN transistor and two resistors. The 10kΩ resistor connects to the digital pin (I used D8) to protect the pin by limiting the amount of current drawn. The 12Ω resistor limits the current through the LEDs. I chose 12Ω to keep the LEDs dim. It’s sitting on my desk inside my field-of-view (that’s kinda the point…) so I didn’t want it to be too dazzling or distracting. They could probably run fine with no limiting resistor here, but then each one would be seeing 2.5v, which is a bit high.

On the right you can see how the bits are wired up. (This is looking “through” the board from above, from the component side.)

The Minor Hoop-Jumping

The Harvest API isn’t exaaaaaactly set up for this. * UPDATE! Harvest is better at this than I thought! See the Update below!* There isn’t a specific “timer status” endpoint I can call, so, instead, I’m pulling in the whole /daily feed. This is a pretty amazing feed, which seems to give the status of every current project and client in your Harvest account… as well as a whole load of other info. In my case, the returned info is *27 printed pages long*. Srsly. I saved it and hit print so I could put it in real terms just for you. (I also hit cancel because trees.)

All I need to know is whether or not the timer is running. When it is running, somewhere in the 27 pages there’s a line containing something like <timer_started_at type="datetime">Thu, 28 Jul 2016 04:28:24 +0000</timer_started_at>. If the device see a line beginning with <timer_started_at, then it turns the lights on and ditches the rest of the feed. If that line’s not there, it means there’s no timer running and the light is turned off. The device doesn’t know it’s not there until it reaches the end of the 27th page, which can take a few seconds to parse (it’s only a small processor!).

For Harvest teams of one, this approach works fine. If there were multiple users on a single Harvest account, this might not work. Or rather, maybe everyone’s light would turn on whenever anyone was tracking time. I dunno. I only have me here to test with!

The Device checks Harvest every 20 seconds or so. I’d love to be able to make it faster so that the light responds more quickly and is more fun to play with, but with the amount of data that Harvest sends back each time, I’d rather not put too much of a load on things. At either end. In internets terms 27 pages isn’t much info, but it seems disrespectful to so many machines to hammer it too hard.

It would be AMAZING if the API had a /timer endpoint that returns info relating to the current timer. Maybe it could the project/task name and elapsed time? That could be really handy for all kinds of other uses!

An Update From Harvest Support

It turns out there is a better way! Trey from Harvest support told me about the “slim=1” url parameter. This leaves out most of the returned data, and only returns the tracked time info. The code is much faster now. Thanks Trey!

The Code

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>

const char* ssid = "SSID";
const char* password = "PASSWORD";
const char* host = "YOURURL.harvestapp.com";
const char* harvestPass = "YOURS-HERE";

// Details about harvestPass are here: http://help.getharvest.com/api/authentication/authentication/http-basic/

const char* page = "/daily?slim=1";
const int httpsPort = 443;
const int ledPin = D8;
const int checkDelayTime = 20000;
double lastCheckTime = 0;

WiFiClientSecure harvestClient;

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, 0);
}

void loop() {
  if (millis() - lastCheckTime > checkDelayTime ) {
    if (WiFi.status() != WL_CONNECTED) startWifi();
    digitalWrite(ledPin, checkHarvest());
  }
}

bool checkHarvest(){
  bool lightsOn = false;
  Serial.print(F("\nconnecting to ")); Serial.println(host);
  if (!harvestClient.connect(host, httpsPort)) {
    Serial.println(F("connection failed"));
  }
  Serial.println(F("Flushing..."));
  harvestClient.flush();
  Serial.println(F("Flushed"));
  Serial.print(F("\nrequesting URL: ")); Serial.println(page);
  harvestClient.print(String("GET ") + page + " HTTP/1.1\r\n" +
    "Host: " + host + "\r\n" +
    "User-Agent: harvestLight\r\n" +
    "Content-Type: application/xml\r\n" +
    "Accept: application/xml\r\n" +
    "Authorization: Basic " + harvestPass + "\r\n" +
    "Connection: close\r\n\r\n");
  Serial.println(F("request sent"));
  while (harvestClient.connected()) {
    String line = harvestClient.readStringUntil('\n');
    if (line.startsWith("<timer_started_at")) {
      Serial.println(line);
      lightsOn = true;
      break;
    }
  }
  lastCheckTime = millis();
  Serial.println(F("Disconnecting..."));
  harvestClient.stop();
  if(!lightsOn) Serial.println(F("Lights Off please"));
  return lightsOn;
}

void startWifi(){
  WiFi.begin(ssid, password);
  Serial.print(F("connecting to ")); Serial.println(ssid);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(F("."));
  }
  Serial.println(F("")); Serial.println("WiFi connected");
  Serial.print(F("IP address: ")); Serial.println(WiFi.localIP());
}

Next Steps