Skip to content

Commit

Permalink
Issue-6: Add support to select monitor location (#9)
Browse files Browse the repository at this point in the history
## Issue:
* #6

## Changes
* Added support to select monitor location
* Dedicated function for converting temperature
* Added additional test cases
* Rename test cases with better names
  • Loading branch information
ljagiello authored Oct 11, 2023
1 parent 4573904 commit 5d6ff89
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 33 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Show AirGradient measurements in MacOS menu bar
Configuration location - `~user/.airdash/config.yaml`
```yaml
token: <secret-token>
locationId: <location-id>
interval: 60
tempUnit: F
```
Expand Down
52 changes: 46 additions & 6 deletions airgradient.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
)

type AirGradientMeasures []struct {
type AirGradientMeasures struct {
LocationID int `json:"locationId"`
LocationName string `json:"locationName"`
Pm01 float64 `json:"pm01"`
Expand All @@ -31,6 +32,27 @@ type AirGradientMeasures []struct {
NoxIndex float64 `json:"noxIndex"`
}

var ErrBadPayload = errors.New("Error unmarshalling JSON")

// getAirGradientAPIURL returns the AirGradient API URL
func getAirGradientAPIURL(locationID int) string {
if locationID != 0 {
return fmt.Sprintf("https://api.airgradient.com/public/api/v1/locations/%d/measures/current", locationID)
}
return fmt.Sprintf("https://api.airgradient.com/public/api/v1/locations/measures/current")
}

// convertTemperature converts the temperature from Celsius to Fahrenheit if the
// temperature unit is set to Fahrenheit
// By default the temperature unit is Celsius
func convertTemperature(temperature float64, tempUnit string) float64 {
if tempUnit == "F" {
return (temperature * 9 / 5) + 32
}
return temperature
}

// fetchMeasures fetches the measures from the AirGradient API
func fetchMeasures(airGradientAPIUrl string, token string) ([]byte, error) {
client := &http.Client{}

Expand Down Expand Up @@ -61,16 +83,34 @@ func fetchMeasures(airGradientAPIUrl string, token string) ([]byte, error) {
}

func getAirGradientMeasures(airGradientAPIUrl string, token string) (AirGradientMeasures, error) {
var arrayAirGradientMeasures []AirGradientMeasures
var airGradientMeasures AirGradientMeasures

payload, err := fetchMeasures(airGradientAPIUrl, token)
if err != nil {
return nil, err
return airGradientMeasures, err
}

var airGradientMeasures AirGradientMeasures
var checkInterface interface{}
json.Unmarshal(payload, &checkInterface)

err = json.Unmarshal(payload, &airGradientMeasures)
if err != nil {
return nil, errors.New("Error unmarshalling JSON")
switch checkInterface.(type) {
case map[string]interface{}:
err = json.Unmarshal(payload, &airGradientMeasures)
if err != nil {
return airGradientMeasures, ErrBadPayload
}
case []interface{}:
err = json.Unmarshal(payload, &arrayAirGradientMeasures)
if err != nil {
return airGradientMeasures, ErrBadPayload
}
if len(arrayAirGradientMeasures) == 0 {
return airGradientMeasures, ErrBadPayload
}
airGradientMeasures = arrayAirGradientMeasures[0]
default:
return airGradientMeasures, ErrBadPayload
}

return airGradientMeasures, nil
Expand Down
68 changes: 62 additions & 6 deletions airgradient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,81 @@ import (
"github.com/stretchr/testify/assert"
)

func TestGetAirGradientAPIURL(t *testing.T) {
var testCases = []struct {
name string
locationID int
expectedURL string
}{
{
"location-id-0",
0,
"https://api.airgradient.com/public/api/v1/locations/measures/current",
},
{
"location-id-12345",
12345,
"https://api.airgradient.com/public/api/v1/locations/12345/measures/current",
},
}
for _, tC := range testCases {
t.Run(tC.name, func(t *testing.T) {
assert.Equal(t, tC.expectedURL, getAirGradientAPIURL(tC.locationID))
})
}
}

func TestConvertTemp(t *testing.T) {
var testCases = []struct {
name string
temp float64
tempUnit string
expected float64
}{
{
"convert-celsius-to-fahrenheit",
20,
"F",
68,
},
{
"no-conversion",
20,
"C",
20,
},
}
for _, tC := range testCases {
t.Run(tC.name, func(t *testing.T) {
assert.Equal(t, tC.expected, convertTemperature(tC.temp, tC.tempUnit))
})
}
}

func TestGetAirGradientMeasures(t *testing.T) {
var testCases = []struct {
name string
payloadFile string
err error
}{
{
"correct-response",
"testdata/correct_response1.json",
"correct-api-v1-locations-measures-current",
"testdata/api-v1-locations-measures-current.json",
nil,
},
{
"correct-api-v1-locations-measures-current-with-more-float64",
"testdata/api-v1-locations-measures-current-with-more-float64.json",
nil,
},
{
"correct-response2",
"testdata/correct_response2.json",
"correct-api-v1-locations-12345-measures-current",
"testdata/api-v1-locations-12345-measures-current.json",
nil,
},
{
"incorrect-response",
"testdata/incorrect_response1.json",
"incorrect-response-404",
"testdata/incorrect-response-404.json",
errors.New("Error unmarshalling JSON"),
},
}
Expand Down
8 changes: 5 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import (
)

type Config struct {
Token string `yaml:"token"`
Interval int `yaml:"interval"`
TempUnit string `yaml:"tempUnit"`
Token string `yaml:"token"`
LocationID int `yaml:"locationId"`
Interval int `yaml:"interval"`
TempUnit string `yaml:"tempUnit"`
}

// LoadConfig loads the config from the given path
func LoadConfig(path string) (*Config, error) {
f, err := os.ReadFile(filepath.Clean(path))
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions config.sample.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
token: <your-secret-token>
locationId: 0
interval: 60
tempUnit: F
7 changes: 6 additions & 1 deletion config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ func TestLoadConfig(t *testing.T) {
name string
configContent []byte
token string
locationID int
interval int
tempUnit string
err error
}{
{
"full-token",
[]byte(fmt.Sprintf("token: \"1234567890\"\ninterval: 60\ntempUnit: \"C\"")),
[]byte(fmt.Sprintf("token: \"1234567890\"\nlocationId: 0\ninterval: 60\ntempUnit: \"C\"")),
"1234567890",
0,
60,
"C",
nil,
Expand All @@ -47,6 +49,7 @@ func TestLoadConfig(t *testing.T) {
[]byte(fmt.Sprintf("token: \"1234567890\"\ntempUnit: \"F\"")),
"1234567890",
0,
0,
"F",
nil,
},
Expand All @@ -55,6 +58,7 @@ func TestLoadConfig(t *testing.T) {
[]byte(`foobar-invalid`),
"",
0,
0,
"",
&yaml.TypeError{Errors: []string{"line 1: cannot unmarshal !!str `foobar-...` into main.Config"}}},
}
Expand All @@ -68,6 +72,7 @@ func TestLoadConfig(t *testing.T) {
cfg, err := LoadConfig(tempFile.Name())
if err == nil {
assert.Equal(t, tC.token, cfg.Token)
assert.Equal(t, tC.locationID, cfg.LocationID)
assert.Equal(t, tC.interval, cfg.Interval)
assert.Equal(t, tC.tempUnit, cfg.TempUnit)
}
Expand Down
26 changes: 9 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import (
"github.com/progrium/macdriver/objc"
)

const AIR_GRADIENT_API_URL = "https://api.airgradient.com/public/api/v1/locations/measures/current"

func main() {
macos.RunApp(launched)
}
Expand All @@ -38,6 +36,8 @@ func launched(app appkit.Application, delegate *appkit.ApplicationDelegate) {
cfg.Interval = 60
}

airGradientAPIURL := getAirGradientAPIURL(cfg.LocationID)

item := appkit.StatusBar_SystemStatusBar().StatusItemWithLength(-1)
objc.Retain(&item)
item.Button().SetTitle("πŸ”„ AirDash")
Expand All @@ -46,32 +46,24 @@ func launched(app appkit.Application, delegate *appkit.ApplicationDelegate) {
for {
select {
case <-time.After(time.Duration(cfg.Interval) * time.Second):
airGradientMeasures, err = getAirGradientMeasures(AIR_GRADIENT_API_URL, cfg.Token)
airGradientMeasures, err = getAirGradientMeasures(airGradientAPIURL, cfg.Token)
if err != nil {
logger.Error("Fetching measures", "error", err)
continue
}
}
logger.Debug("AirGradientMeasures", "measures", airGradientMeasures)

if len(airGradientMeasures) == 0 {
logger.Error("No measurements found")
return
}

logger.Debug("AirGradientMeasures", "measures", airGradientMeasures[0])

temperature := airGradientMeasures[0].Atmp
if cfg.TempUnit == "F" {
temperature = (airGradientMeasures[0].Atmp * 9 / 5) + 32
}
// convert the temperature to the desired unit
temperature := convertTemperature(airGradientMeasures.Atmp, cfg.TempUnit)

// updates to the ui should happen on the main thread to avoid segfaults
dispatch.MainQueue().DispatchAsync(func() {
item.Button().SetTitle(fmt.Sprintf("🌑️ %.2f πŸ’¨ %.0f πŸ’§ %.1f 🫧 %.0f",
temperature,
airGradientMeasures[0].Pm02,
airGradientMeasures[0].Rhum,
airGradientMeasures[0].Rco2,
airGradientMeasures.Pm02,
airGradientMeasures.Rhum,
airGradientMeasures.Rco2,
))
})
}
Expand Down
1 change: 1 addition & 0 deletions testdata/api-v1-locations-12345-measures-current.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"locationId":12345,"locationName":"Test Loc","pm01":null,"pm02":5,"pm10":null,"pm003Count":null,"atmp":23.3,"rhum":53,"rco2":537,"tvoc":93.979355,"wifi":-52,"timestamp":"2023-10-11T04:50:46.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"aabb12","firmwareVersion":null,"tvocIndex":100,"noxIndex":1}
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit 5d6ff89

Please sign in to comment.