Last active
September 27, 2020 21:14
-
-
Save wcharczuk/9cc246bc6a70ae03a5f4d9f849c69779 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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