November 2, 2018

Weather in an SSID

I have a server at home that I use for testing odd hardware configurations, so it tends to have a bunch of junk attached to it. Among that junk is a collection of several USB wireless dongles that are each configured to broadcast WiFi networks. None of these cheap dongles are suitable for actual use as a high speed access point, but they do function as access points nonetheless. The other day I set out to think of interesting projects I could use them for, and this post will detail one of those projects.

The idea was to get an access point to update its SSID with the current weather.

Background

I'm gonna describe how my system was set up before starting this project.

For starters, all these access points are handled by hostapd. I have a hostapd config file for each wireless dongle's interface, and have my network interfaces file configured to bring up hostapd with the interface. This setup lets me create visible WiFi access points with a simple ifup wlan1 which will bring up the wlan1 interface normally and also start a hostapd instance using the hostapd.wlan1.conf configuration file.

If you're curious, an example /etc/network/interfaces entry for the wlan1 interface that I used for this project is as follows:

auto wlan1
iface wlan1 inet static
  hostapd /etc/hostapd/hostapd.wlan1.conf
  address 10.12.0.1
  netmask 255.255.0.0

A similar entry exists for each additional wireless interface, but we aren't going to be using any others for this.

The hostapd directive in the third line is handled by a hostapd hook in /etc/network/if-pre-up.d/, which as far as I'm aware was installed along with the hostapd package in Debian.

Assembling the Parts

To accomplish our task, we just need to set the SSID in the hostapd configuration to something representing the current weather, and then cycle the interface to restart hostapd. Simple.

I started off by making a template configuration file (which was just the config file I was already using with the SSID replaced with a template token)

interface=wlan1
ssid={{WEATHER}}
channel=3
hw_mode=g
wpa=2
wpa_passphrase=<some password>
wpa_key_mgmt=WPA-PSK WPA-PSK-SHA256

Great. Now we just need to load that file, string replace {{WEATHER}} with the ssid we want, write the result to /etc/hostapd/hostapd.wlan1.conf, and ifdown/ifup wlan1.

But how do we get the weather? I pulled out some code from the weather module of an old IRC bot I made back in the day that still had an OpenWeatherMap API token and some usage logic to boot. Lifting the code straight from there was the simplest thing to do, and I ended up with a URL that would give me back all the information I needed as a JSON object.

At this point, I have a little node.js script that hits the weather API every minute, extracts the local temperature and weather, writes it into the template configuration string, writes that result to the actual config file, and kicks the interface to get hostapd to update. To minimize the access point downtime, the script only takes action if the SSID should actually change.

The script outputting some debug information every time it changes the SSID

But of course, now we need emojis.

A simple map of weather strings to emojis, and a little logic to conditionally add a moon emoji at night, and we can easily add them to the SSID we generate. Hostapd is happy to support UTF8 in the SSID you give it.

Results

Here are a couple examples of the network as seen from my phone.

SSID during a cloudy day SSID at night

Code

The original version of this was cobbled together in 20 minutes and was disgusting enough that I didn't feel comfortable posting it. Since then, I've spent a little time cleaning it up and now I feel like it's presentable.

const { writeFileSync } = require('fs')
const { exec } = require('child_process')
const request = require('request')

const APIKEY = process.env["APIKEY"]
const ZIP = process.env["ZIP"]
const IFACE = process.env["IFACE"]
const HOSTAPD_TEMPL = `interface=wlan1\nssid={{WEATHER}}\nchannel=3\nhw_mode=g\nwpa=2\nwpa_passphrase=${Math.random()}\nwpa_key_mgmt=WPA-PSK WPA-PSK-SHA256\n`

const emojis = {
  'rain': '🌧',
  'drizzle': '🌧',
  'tornado': '🌪',
  'snow': '❄',
  'thunderstorm': '⛈',
  'few clouds': '🌤',
  'clouds': '☁️',
  'clear': '☀️',
  'night': '🌙'
}

let get_ssid = cb => {
  request(`http://api.openweathermap.org/data/2.5/weather?APPID=${APIKEY}&units=imperial&zip=${ZIP}`, (err, res, body) => {
    let b = JSON.parse(body)
    let weather = b.weather[0].main.toLowerCase()
    let desc = b.weather[0].description.toLowerCase()
    let e = emojis[desc] || emojis[weather]
    if (!e) return
    let now = Date.now() / 1000
    if (now < b.sys.sunrise || now > b.sys.sunset) {
      e = emojis.night + ((['clouds', 'clear'].includes(weather)) ? '' : e)
    }
    cb(`It's ${Math.round(b.main.temp)}°F and ${e}`)
  })
}

let state = ''

let update_ssid = () => {
  get_ssid(ssid => {
    if (ssid && state != ssid) {
      let conf = HOSTAPD_TEMPL.replace('{{WEATHER}}', ssid)
      writeFileSync(`/etc/hostapd/hostapd.${IFACE}.conf`, conf)
      exec(`ifdown ${IFACE}; sleep 1; ifup ${IFACE}`)
      state = ssid
    }
  })
}

update_ssid()
setInterval(update_ssid, 1000*60)

process.on('uncaughtException', e => console.log(e))