Category: Geek

Chicken Cam: Incubator Edition

March 4, 2018 » Geek, Life

It’s been over a year since we’ve had chickens and we’ve missed them, so this Christmas we got Lizzy and Charlotte an incubator so that we could try hatching some this spring.

When we went to purchase eggs, we found that you could most easily get them 10 at a time from the hatchery we have used in the past, Murray McMurray. Since the incubator we got the girls could only hold seven, we would need something for the other three. Some searching found that you could use a styrofoam cooler and a lamp to create a makeshift incubator, so I planned on that.

Once I had a plan to create an incubator, I knew I would have to overcomplicate things. Four years ago I built a webcam for our chicks so I figured I would do that this time too. Also, just setting a lamp and thermometer in and hoping for the best seemed like a potential waste of good eggs, so I wanted to monitor the temperature and humidity, and regulate them.

My initial design was a Raspberry Pi connected to a cheap DHT11 temperature and humidity sensor, controlling a relay that could turn the light on and off. All of it would be hooked up through a PID controller to keep the temperatures right where we want them. Eventually, I added a thermocouple with a MAX6675 for more accurate temperature readings.

Raspberry Pi, Relay and a mess of wires.

The server side would be designed similarly to the previous chicken cam, except written in Go. The stats would be tracked in InfluxDB and Grafana would be used for viewing them.

After I got all the parts I did a little testing, then soldered things up and tested it to see how it ran.

Initially I wrote everything in Go, but the DHT11 reading was very spotty. Sometimes it would respond once every few seconds, and sometimes it would go a minute or more failing to read. I wired on a second DHT11 and tried reading from both, but I didn’t get that much better performance.

Eventually I tried them from the Adafruit Python library and had much better luck, so I decided to just read those from Python and send them to my main Go application for consumption. I still have trouble with the DHT11’s, but I suspect it’s my fault more than the sensors fault.

My next issue was that it was extremely jittery, the readings would vary by degrees one second to another, so I collected readings in batches of 5 seconds then averaged them. That smoothed it out enough that graphs looked reasonable.

On. Off. On. Off. On. Off.

Temperature was now well regulated, but the air wasn’t humid enough. I switched to a sponge and found I could manage it much easier that way. I briefly tried a 40W bulb thinking I could spend more time with the lamp off, but temperatures still plunged at the same rate when the light was off, so I mostly just created quicker cycles.

After putting the 25W bulb back in, I still wanted a longer, smoother cycle, so I wrapped up a brick (for cleanliness) and stuck that in there. That got me longer cycles with better recovery at the bottom, it didn’t get too cold before the lamp came back on. Some slight improvements to the seal of my lid helped as well. I had trouble with condensation and too much humidity, but some vent holes and better water management took care of that.

Before the brick.

After the brick.

For the server side, I mostly duplicated the code from the previous Chicken cam, but in Go. Then I used the InfluxDB library to get the most recent temperature and humidity readings for display.

At this point, I felt ready for the eggs, which was good because they had arrived! We placed them in the incubator and we’re just waiting now. On day 8 we candled them with a homebuilt lamp i.e. a cardboard box with a hole cut in it.


Things seem to be progressing well so far, so here’s hoping something hatches!


September 14, 2017 » Geek

When Rdio shut down, I tried a few services before landing on Google Play. It’s not perfect, but it’s good enough and it’s better than Spotify. One thing that seemed lacking was a desktop application, but that need was neatly filled by the excellent GPDMP.

One lesser known feature of GPDMP is the JSON API, which manifests as a simple JSON file that the application updates with information about the playback. When Slack announced custom statuses, I though back to the days of instant messaging and the integrations that set your status to the song you were playing.


Implementing the link from GPDMP to Slack was, in all, a fairly simple matter. First, I looked at the JSON file to get a feel for the structure.

    "playing": true,
    "song": {
        "title": "Freeze Me",
        "artist": "Death From Above 1979",
        "album": "Outrage! Is Now",
        "albumArt": "https://lh3.go...-e100"
    "rating": {
        "liked": false,
        "disliked": false
    "time": {
        "current": 363509,
        "total": 198000
    "songLyrics": null,
    "shuffle": "NO_SHUFFLE",
    "repeat": "NO_REPEAT",
    "volume": 100

Short and sweet! Now to represent that in Go for decoding.

type Song struct {
	Title    string
	Artist   string
	Album    string
	AlbumArt string

type PlaybackJSON struct {
	Playing bool
	Song    Song
	Rating  struct {
		Liked    bool
		Disliked bool
	Time struct {
		Current int
		Total   int
	SongLyrics string
	Shuffle    string
	Repeat     string
	Volume     int

I didn’t need to represent all the elements, but it’s a small structure so I went ahead with it. I didn’t embed Song because I wanted to write an equality test for that struct on it’s own. That will get used later on.

func (a Song) Equal(b Song) bool {
	return a.Title == b.Title && a.Artist == b.Artist && a.Album == b.Album

Next, I needed a way to monitor that file for updates, which GPDMP does fairly often. fsnotify was the obvious choice, and an easy drop in.
I added a time based debounce so that we don’t read the file on every update, which would be excessive. This will delay updates by up to whatever debounce is set to, but I’m okay with that trade off.

watcher, err := fsnotify.NewWatcher()
if err != nil {
defer watcher.Close()

go func() {
	var lastRead time.Time

	for {
		select {
		case event := <-watcher.Events:
			if event.Op&fsnotify.Write == fsnotify.Write {
				if time.Now().After(lastRead.Add(debounce)) {
					lastRead = time.Now()
		case err := <-watcher.Errors:
			log.Println("error:", err)

err = watcher.Add(gp.Path)
if err != nil {

Inside that debounce (at line 16) we open the file, decode it to a new struct and, if it's playing, pass it off to a channel.

f, err := os.Open(event.Name)
if err != nil {

dec := json.NewDecoder(f)
pb := PlaybackJSON{}

err = dec.Decode(&pb)
if err != nil {

if pb.Playing {
	updates <- pb.Song

So, that's it for getting updates from GPDMP! Less than 100 lines, formatted. Now I needed to watch that update channel and post changes in status to Slack.

I found an excellent Slack API client on a different project, so I grabbed that. I started by building a little struct to hold my client and state.

type Slack struct {
	Client       *slack.Client
	CurrentSong  Song
	Set          bool
	InitialText  string
	InitialEmoji string

Then, during client initialization, we get the current custom status for the user and save it. This way, when you pause your music, it will revert to whatever you had set before.

func (s *Slack) Init() {
	auth, err := s.Client.AuthTest()
	if err != nil {

	user, err := s.Client.GetUserInfo(auth.UserID)
	if err != nil {

	s.InitialText = user.Profile.StatusText
	s.InitialEmoji = user.Profile.StatusEmoji
	log.Printf("Initial status: %s %s", s.InitialEmoji, s.InitialText)

Once it is initialized, we just need to range over our updates channel and post them to Slack when it changes. We set a timeout, because the GPDMP client won't send updates when the song is paused, or if the app quits updating the file (i.e. you quit GPDMP). By putting the logic for the timeout on this side, we have less to pass over the channel, and we can revert properly if something goes awry in the api reading goroutine.

func (s *Slack) Sync(emoji string, updates chan Song, revert_after time.Duration) {
	for {
		select {
		case song := <-updates:
			if !s.CurrentSong.Equal(song) {
				log.Printf("Sync: %s by %s\n", song.Title, song.Artist)
				s.Client.SetUserCustomStatus(fmt.Sprintf("%s by %s", song.Title, song.Artist), emoji)
				s.CurrentSong = song
				s.Set = true
		case <-time.After(revert_after):
			if s.Set {
				log.Printf("Reverting Status: %s %s\n", s.InitialEmoji, s.InitialText)
				s.Client.SetUserCustomStatus(s.InitialText, s.InitialEmoji)
				s.CurrentSong = Song{}
				s.Set = false

A little bit of glue in main and it's ready!

func main() {

	api := NewSlack(os.Getenv("SLACK_TOKEN"))
	gpdmp := &GPDMPAPI{os.Getenv("GPDMPAPI_PATH")}


	updates := make(chan Song)
	done := make(chan bool)

	go gpdmp.Watch(updates, done, 5*time.Second)
	go api.Sync(config.Emoji, updates, 15*time.Second)

You can browse the source and grab your copy at

MAC Randomizer Alfred Script

December 19, 2016 » Geek

A recent conversation I had dealt with free wifi that limited the amount of time you could use it before it kicked you off. Now, while I support the right of wifi providers to do as they please, it’s an interesting question. AFAIK most tracking of that sort is done based on MAC addresses, which you can easily spoof if you want.

I wrote up a quick Alfred workflow that shells out from Python to do the real work. Note that if your wifi interface isn’t called en0 this won’t work for you.

Workflow Overview

The first script shells out to ifconfig to get the current address. Which gives output like the following. We are interested in that ether f4:5c:89:b3:37:e1 line. The first three octets are of a MAC are the Organizationally Unique Identifier (OUI) and we don’t need to change those, what we have is valid already.

en0: flags=8863 mtu 1500
	ether f4:5c:89:b3:37:e1
	inet6 fe80::8da:f24a:a0bb:3b7a%en0 prefixlen 64 secured scopeid 0x4
	inet netmask 0xffffff00 broadcast
	nd6 options=201
	media: autoselect
	status: active

Our script captures the OUI, then generates three more octets for the rest of the address, and prints it out.

from subprocess import check_output
from re import compile
from random import randint

MATCHER = compile("\W*ether ([a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2})")
output = check_output(["ifconfig", "en0"])

mac = None

for line in output.split("\n"):
    match = MATCHER.match(line)
    if match is not None:
        mac = match.groups()[0]

prefix = mac[:8]

print "%s:%x:%x:%x" % (prefix, randint(0, 255), randint(0, 255), randint(0, 255))

Next we need to actually set this new random MAC. This is a privileged operation, so it we passed it directly to ifconfig it would error out. Long story short, if we want a nice authorization dialog we have to pass through applescript, russian nesting doll style.

osascript -e  "do shell script \"sudo ifconfig en0 ether {query} >/dev/null;\" with administrator privileges"

I also added a way to reset it to the hardware value. The networksetup command handily has that for the taking. We just shell out, capture it and pass it through to ifconfig again.

from subprocess import check_output
from re import compile

MATCHER = compile("Ethernet Address: ([a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2})")
output = check_output(["networksetup", "-getmacaddress", "en0"])
match = MATCHER.match(output)
print match.groups()[0]

You can download this workflow, comments and improvements appreciated.

Anonymous Code of Conduct Reporting With Twilio

August 29, 2016 » Geek

I’m lucky to be one of the organizers of NEJS Conf, a great little JavaScript & frontend conference here in Omaha, NE.

One of the core values we’ve held since the beginning of our conference was for it to be diverse, respectful and safe. To that end we adopted a Code of Conduct from the very beginning, based on the excellent JSConf example.

Our first year, we identified specific volunteers as our CoC points of contact. It seemed like a good plan, but our only report that year came via a circuitous route, which may have been a result of the face-to-face reporting we had defaulted to.

This spring I got to attend Twilio’s SIGNAL conference, and one neat thing they had in their Code of Conduct was an anonymous reporting phone line. Sounds like a good idea, and something fun to build!

The plan is simple: add a Twilio backed phone number which anonymizes incoming SMS and calls, then forwards them to the code of conduct reporting volunteer. Twilio makes this easy. At it’s core it’s just two TwiML files, one for SMS and one for Voice.

The SMS response contains the original message, a destination number to send it to (i.e. the CoC volunteer), a unique ID per reporter, and a link to the web interface. Behind the scenes we are doing a little but of work to assign the ID, match up numbers to destinations, etc, but not a lot of work total.

<?xml version="1.0" encoding="UTF-8" ?>
  <Message to="{{ destination_number }}">[{{ reporter_id }}] {{ message_body }}

{{ link }}</Message>

Voice is even simpler. Here we just connect the call to the CoC volunteer, and spoof the caller ID with the hotline’s number.

<?xml version="1.0" encoding="UTF-8"?>
  <Say>Connecting, one moment.</Say>
  <Dial record="true" callerId="{{ caller_id }}">{{ number.destination }}</Dial>

That’s the core of it, only took an evening to get things running. As I hinted above, I added a web interface for replying to reporters, as well as seeing the entire interaction in one place.

SMS Reporting Web Interface Voice Reporting

If you’d like to run this for your event, the source is all on github.

Tags: , , ,

Using Let’s Encrypt With Dreamhost

December 8, 2015 » Geek


As pointed out in the comments, Dreamhost now supports Let’s Encrypt in the panel. No more workaround needed!

Let’s Encrypt has entered public beta, which means I should probably play with it!

This website is hosted on Dreamhost, which has a round about way of installing SSL certs, but it’s not too bad.

First, you have to go to your Dreamhost panel, then “Secure Hosting” and select “Add Secure Hosting”

Add Secure Hosting

From here, you pick your domain you want to secure. It’s a little bit wonky, in that it doesn’t show www. domains as subdomains in this list, so if you use that, you’ll need to just select the parent domain.

Doing this will issue you a self-signed certificate which will throw up scary browser warnings. We will fix that next.

I chose to run Let’s Encrypt on my laptop, so I followed the user guide to get things installed. Basically just a git clone.

Next you have to begin the request process.

./letsencrypt-auto certonly --manual --debug

  • certonly states that we only want a certificate generated, not installed.
  • --manual means that we are going to manually authenticate it.
  • --debug is used with the OS X version because it is experimental.

This will probably download some junk with homebrew, then it’s going to ask you some questions, the greatest of which is what domain you want to use.

With this in hand, it will generate an authentication string that you need to put into a file on the server.

[claw]$ cd
[claw]$ mkdir -p .well-known/acme-challenge/
[claw]$ echo -n "EVgSHY-sQeMAy4TTx_-jjrx-mR3Dmr4M5Byt9vBKcLE.9wpTWpx1Ghg8yXEMASBfWbfU-fGgjG6D-ixF4ip3cDU" > .well-known/acme-challenge/EVgSHY-sQeMAy4TTx_-jjrx-mR3Dmr4M5Byt9vBKcLE

Once you do that, it spits out your certificate into /etc/letsencrypt/live/[domain]

/etc/letsencrypt/live/ ✪ ls
cert.pem	chain.pem	fullchain.pem	privkey.pem

Back on the Dreamhost panel, you’ll want to click on “Edit” for the domain we are securing, then select “Manual Configuration”.

Edit Secure Hosting

You can clear the CSR field and then into the “Certificate” field, enter the content from cert.pem.

Into “Intermediate Certificate” I placed the contents of chain.pem

Lastly, we have to change the format of the private key file to one Dreamhost understands.

/etc/letsencrypt/live/ ✪ openssl rsa -in privkey.pem -out privkey.key
writing RSA key

Then we paste privkey.key into the Dreamhost interface for “Private Key”, save and wait for our new certificate to get installed.

Editing Certificates

It’s magic!

Add Secure Hosting

Now I just have to fix all my asset URLs too…

Tags: , , ,