-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmonnit.go
195 lines (167 loc) · 5.33 KB
/
monnit.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package main
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"os"
"sync"
"time"
)
const DATE_FORMAT = "Mon, 02 Jan 2006 15:04:05"
const CACHE_FILE = "monnit.json"
type Monnit struct {
sync.RWMutex
sensorId string
apiKeyId string
apiSecretKey string
apiUrl string
lastData *SensorDataMessages
}
func NewMonnit(sensorID, apiKeyID, apiSecretKey, url string, interval time.Duration) *Monnit {
monnit := Monnit{
sensorId: sensorID,
apiKeyId: apiKeyID,
apiSecretKey: apiSecretKey,
apiUrl: url,
}
// Load cached data
if f, err := os.Open(CACHE_FILE); err == nil {
if err = json.NewDecoder(f).Decode(&monnit.lastData); err != nil {
slog.Error("unable to restore cached values", "error", err)
}
slog.Info("loaded cached Monnit data", "cache", CACHE_FILE)
slog.Info("latest reading", "measurement", monnit.LastReading())
} else {
slog.Info("cached Monnit data not found", "cache", CACHE_FILE)
if err = monnit.LoadData(); err != nil {
slog.Warn("problem loading data on startup", "error", err)
}
slog.Info("latest reading", "measurement", monnit.LastReading())
}
go monnit.refresh(interval)
return &monnit
}
// refresh automatically updates sensor data at the interval
func (m *Monnit) refresh(interval time.Duration) {
ticker := time.NewTicker(interval)
for range ticker.C {
err := m.LoadData()
if err != nil {
slog.Error("failed to load data", "error", err)
}
slog.Debug("refreshed data", "interval", interval)
slog.Info("latest reading", "measurement", m.LastReading())
}
}
func (m *Monnit) LoadData() error {
m.Lock()
defer m.Unlock()
sdm := SensorDataMessages{
LastUpdated: time.Now(),
}
// Requesting from current time to seven days in the past
toDate := time.Now().UTC()
fromDate := toDate.AddDate(0, 0, -7).UTC()
req, err := http.NewRequest("GET", m.apiUrl, nil)
if err != nil {
slog.Error("error creating request", "error", err)
return err
}
// Set API keys in HTTP headers
req.Header.Set("APIKeyID", m.apiKeyId)
req.Header.Set("APISecretKey", m.apiSecretKey)
// Pass sensor ID and date range as query params
q := req.URL.Query()
q.Add("sensorID", m.sensorId)
q.Add("fromDate", fromDate.Format(DATE_FORMAT))
q.Add("toDate", toDate.Format(DATE_FORMAT))
req.URL.RawQuery = q.Encode()
slog.Debug("loading data from Monnit API", "url", req.URL.String(),
"fromDate", fromDate.Format(DATE_FORMAT),
"toDate", toDate.Format(DATE_FORMAT),
)
// Make request
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
slog.Error("error sending request", "error", err)
return err
}
defer res.Body.Close()
// Write cache while reading response body
f, _ := os.Create(CACHE_FILE)
r := io.TeeReader(res.Body, f)
// Parse response
err = json.NewDecoder(r).Decode(&sdm)
if err != nil {
slog.Error("error decoding JSON response", "error", err)
return err
}
// Update data
m.lastData = &sdm
return nil
}
func (m *Monnit) LastReading() *SensorDataMessage {
m.RLock()
defer m.RUnlock()
if m.lastData == nil {
return &SensorDataMessage{}
}
return m.lastData.GetLast()
}
// ToApiResponse converts the Monnit's last sensor data messages to an ApiResponse format containing API messages.
func (m *Monnit) ToApiResponse() ApiResponse {
m.RLock()
defer m.RUnlock()
var apiMessages []ApiMessage
for _, m := range m.lastData.Messages {
apiMessages = append(apiMessages, m.ToApiMessage())
}
return apiMessages
}
// SensorDataMessages represents the structure for sensor data communication.
// It contains the method used and a slice of SensorDataMessage structs.
type SensorDataMessages struct {
Method string `json:"Method"`
Messages []SensorDataMessage `json:"Result"`
LastUpdated time.Time
}
func (sdm *SensorDataMessages) GetLast() *SensorDataMessage {
if len(sdm.Messages) == 0 {
return &SensorDataMessage{}
}
return &sdm.Messages[0]
}
type SensorDataMessage struct {
DataMessageGUID string `json:"DataMessageGUID"`
SensorID int `json:"SensorID"`
MessageDate MessageDate `json:"MessageDate"`
State int `json:"State"`
SignalStrength int `json:"SignalStrength"`
Voltage float64 `json:"Voltage"`
Battery int `json:"Battery"`
Data string `json:"Data"`
DisplayData string `json:"DisplayData"`
Temperature Temperature `json:"PlotValue"`
MetNotificationRequirements bool `json:"MetNotificationRequirements"`
GatewayID int `json:"GatewayID"`
DataValues string `json:"DataValues"`
DataTypes string `json:"DataTypes"`
PlotValues string `json:"PlotValues"`
PlotLabels string `json:"PlotLabels"`
}
func (m SensorDataMessage) LogValue() slog.Value {
return slog.GroupValue(
slog.Time("date", time.Time(m.MessageDate)),
slog.String("temperature", m.Temperature.String()),
slog.Int("signal_strength", m.SignalStrength),
)
}
// ToApiMessage converts a SensorDataMessage to an ApiMessage, mapping relevant fields such as temperature and date.
func (m *SensorDataMessage) ToApiMessage() ApiMessage {
return ApiMessage{
Temperature: m.Temperature,
LastModified: m.MessageDate,
}
}