Skip to content

Instantly share code, notes, and snippets.

@wcharczuk
Last active September 27, 2020 21:14
Show Gist options
  • Save wcharczuk/9cc246bc6a70ae03a5f4d9f849c69779 to your computer and use it in GitHub Desktop.
Save wcharczuk/9cc246bc6a70ae03a5f4d9f849c69779 to your computer and use it in GitHub Desktop.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"sort"
"sync"
"time"
)
var awairSensors = map[string]string{
"Bedroom": "192.168.53.1",
"Living Room": "192.168.53.235",
}
func main() {
http.Handle("/", handler(getSensorData))
log.Println("http server listening on:", bindAddr())
if err := http.ListenAndServe(bindAddr(), nil); err != nil {
log.Fatal(err)
}
}
func bindAddr() string {
if value := os.Getenv("BIND_ADDR"); value != "" {
return value
}
return ":8080"
}
type handler func(http.ResponseWriter, *http.Request)
func (h handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
start := time.Now()
irw := &responseWriter{ResponseWriter: rw}
defer func() {
log.Println(fmt.Sprintf("%s %d %s %v", r.URL.Path, irw.statusCode, formatContentLength(irw.contentLength), time.Since(start)))
}()
h(irw, r)
}
const (
sizeofByte = 1 << (10 * iota)
sizeofKilobyte
sizeofMegabyte
sizeofGigabyte
)
func formatContentLength(contentLength uint64) string {
if contentLength >= sizeofGigabyte {
return fmt.Sprintf("%0.2fgB", float64(contentLength)/float64(sizeofGigabyte))
} else if contentLength >= sizeofMegabyte {
return fmt.Sprintf("%0.2fmB", float64(contentLength)/float64(sizeofMegabyte))
} else if contentLength >= sizeofKilobyte {
return fmt.Sprintf("%0.2fkB", float64(contentLength)/float64(sizeofKilobyte))
}
return fmt.Sprintf("%dB", contentLength)
}
type responseWriter struct {
http.ResponseWriter
statusCode int
contentLength uint64
}
func (rw *responseWriter) WriteHeader(statusCode int) {
rw.statusCode = statusCode
rw.ResponseWriter.WriteHeader(statusCode)
}
func (rw *responseWriter) Write(data []byte) (n int, err error) {
n, err = rw.ResponseWriter.Write(data)
rw.contentLength += uint64(n)
return
}
func getSensorData(rw http.ResponseWriter, r *http.Request) {
sensorData := map[string]*Awair{}
var sensors []string
var resultsMu sync.Mutex
var wg sync.WaitGroup
wg.Add(len(awairSensors))
errors := make(chan error, len(awairSensors))
for sensor, host := range awairSensors {
go func(s, h string) {
defer wg.Done()
data, err := getAwairData(r.Context(), h)
if err != nil {
errors <- err
return
}
resultsMu.Lock()
sensorData[s] = data
sensors = append(sensors, s)
resultsMu.Unlock()
}(sensor, host)
}
wg.Wait()
if len(errors) > 0 {
http.Error(rw, fmt.Sprintf("error fetching data; %v", <-errors), http.StatusInternalServerError)
return
}
sort.Strings(sensors)
rw.Header().Add("Content-Type", "text/plain; charset=utf-8")
rw.WriteHeader(http.StatusOK)
for _, sensor := range sensors {
data, ok := sensorData[sensor]
if !ok {
continue
}
fmt.Fprintf(rw, "awair_score{sensor=%q} %f\n", sensor, data.Score)
fmt.Fprintf(rw, "awair_dew_point{sensor=%q} %f\n", sensor, data.DewPoint)
fmt.Fprintf(rw, "awair_temp{sensor=%q} %f\n", sensor, data.Temp)
fmt.Fprintf(rw, "awair_humid{sensor=%q} %f\n", sensor, data.Humid)
fmt.Fprintf(rw, "awair_co2{sensor=%q} %f\n", sensor, data.CO2)
fmt.Fprintf(rw, "awair_voc{sensor=%q} %f\n", sensor, data.VOC)
fmt.Fprintf(rw, "awair_voc_baseline{sensor=%q} %f\n", sensor, data.VOCBaseline)
fmt.Fprintf(rw, "awair_voc_h2_raw{sensor=%q} %f\n", sensor, data.VOCH2Raw)
fmt.Fprintf(rw, "awair_voc_ethanol_raw{sensor=%q} %f\n", sensor, data.VOCEthanolRaw)
fmt.Fprintf(rw, "awair_pm25{sensor=%q} %f\n", sensor, data.PM25)
fmt.Fprintf(rw, "awair_pm10_est{sensor=%q} %f\n", sensor, data.PM10Est)
}
return
}
// Awair is the latest awair data from a sensor.
type Awair struct {
Timestamp time.Time `json:"timestamp"`
Score float64 `json:"score"`
DewPoint float64 `json:"dew_point"`
Temp float64 `json:"temp"`
Humid float64 `json:"humid"`
CO2 float64 `json:"co2"`
VOC float64 `json:"voc"`
VOCBaseline float64 `json:"voc_baseline"`
VOCH2Raw float64 `json:"voc_h2_raw"`
VOCEthanolRaw float64 `json:"voc_ethanol_raw"`
PM25 float64 `json:"pm25"`
PM10Est float64 `json:"pm10_est"`
}
func getAwairData(ctx context.Context, host string) (*Awair, error) {
const path = "/air-data/latest"
req := http.Request{
Method: "GET",
URL: &url.URL{
Scheme: "http",
Host: host,
Path: path,
},
}
var data Awair
err := getJSON(ctx, &req, &data)
if err != nil {
return nil, err
}
return &data, nil
}
func getJSON(ctx context.Context, req *http.Request, output interface{}) error {
req = req.WithContext(ctx)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if statusCode := res.StatusCode; statusCode < http.StatusOK || statusCode > 299 {
return fmt.Errorf("non-200 returned from remote")
}
if err := json.NewDecoder(res.Body).Decode(output); err != nil {
return err
}
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment