ecodash/api.go
MassiveBox 52ba0ea4c1
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
Improve SQLite
- Now the cache isn't cleared and fetched from zero each update, but only when there's missing information for a day
- Fixed a bug that prevented new users from saving changes in the config

NOTE: Turns out that HomeAssistant's API only returns data for the last 10 days. Looks like we will have to improve the caching system to work around this.
2023-01-04 15:57:16 +01:00

318 lines
6.9 KiB
Go

package main
import (
"encoding/json"
"errors"
"io"
"log"
"math"
"net/http"
"net/url"
"strconv"
"time"
)
type HistoryResult []struct {
State string `json:"state"`
LastUpdated time.Time `json:"last_updated"`
}
func dayStart(t time.Time) time.Time {
hours, minutes, seconds := t.Clock()
return t.Add(-(time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second))
}
func (config Config) queryHistory(entityID string, startTime, endTime time.Time) (HistoryResult, error) {
req, err := http.NewRequest("GET", config.HomeAssistant.BaseURL+
"/api/history/period/"+url.QueryEscape(startTime.Format(time.RFC3339))+
"?filter_entity_id="+entityID+
"&end_time="+url.QueryEscape(endTime.Format(time.RFC3339)), /*+
"&minimal_response",*/nil)
if err != nil {
return HistoryResult{}, err
}
req.Header.Add("Authorization", "Bearer "+config.HomeAssistant.ApiKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return HistoryResult{}, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return HistoryResult{}, errors.New("got a non-200 status code. Check the correctness of sensors IDs -" + resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return HistoryResult{}, err
}
var result []HistoryResult
err = json.Unmarshal(body, &result)
if err != nil {
return HistoryResult{}, err
}
if len(result) != 1 {
return HistoryResult{}, nil
}
return result[0], nil
}
// t can be any time during the desired day.
func (config Config) getDayHistory(entityID string, t time.Time) (HistoryResult, error) {
hours, minutes, seconds := t.Clock()
endTime := t.Add(time.Duration(23-hours)*time.Hour + time.Duration(59-minutes)*time.Minute + time.Duration(59-seconds)*time.Second)
return config.queryHistory(entityID, dayStart(t), endTime)
}
type DayData struct {
DayNumber int
DayTime time.Time
Measurements int
Value float32
High float32
Low float32
}
func (config Config) historyAverageAndConvertToGreen(entityID string, t time.Time) (DayData, error) {
history, err := config.getDayHistory(entityID, t)
if err != nil {
return DayData{}, err
}
var day DayData
for _, historyChange := range history {
val, err := strconv.ParseFloat(historyChange.State, 32)
if err != nil {
continue
}
day.Value += float32(val)
day.Measurements++
}
day.Value = 100 - (day.Value / float32(day.Measurements))
return day, nil
}
func (config Config) historyBulkAverageAndConvertToGreen(entityID string, startTime, endTime time.Time) ([]DayData, error) {
history, err := config.queryHistory(entityID, startTime, endTime)
if err != nil {
return nil, err
}
var days []DayData
for _, historyChange := range history {
val, err := strconv.ParseFloat(historyChange.State, 32)
if err != nil {
continue
}
value := float32(val)
var found bool
dayNo := historyChange.LastUpdated.Local().Day()
for key, day := range days {
if dayNo == day.DayNumber {
found = true
day.Value += value
day.Measurements++
days[key] = day
}
}
if !found {
days = append(days, DayData{
DayNumber: dayNo,
DayTime: dayStart(historyChange.LastUpdated.Local()),
Measurements: 1,
Value: value,
})
}
}
for key, day := range days {
// by using 100 - value we get the percentage of green energy instead of the percentage of fossil-generated energy
day.Value = 100 - (day.Value / float32(day.Measurements))
days[key] = day
}
days = fillMissing(days, startTime, endTime)
return days, nil
}
func (config Config) historyDelta(entityID string, t time.Time) (DayData, error) {
history, err := config.getDayHistory(entityID, t)
if err != nil {
return DayData{}, err
}
var day DayData
for _, historyChange := range history {
val, err := strconv.ParseFloat(historyChange.State, 32)
if err != nil {
continue
}
value := float32(val)
if value > day.High {
day.High = value
}
if value < day.Low || day.Low == 0 {
day.Low = value
}
}
day.Value = day.High - day.Low
return day, nil
}
func (config Config) historyBulkDelta(entityID string, startTime, endTime time.Time) ([]DayData, error) {
history, err := config.queryHistory(entityID, startTime, endTime)
if err != nil {
return nil, err
}
var days []DayData
for _, historyChange := range history {
if historyChange.State != "off" {
val, err := strconv.ParseFloat(historyChange.State, 32)
if err != nil {
continue
}
value := float32(val)
var found bool
dayNo := historyChange.LastUpdated.Local().Day()
for key, day := range days {
if dayNo == day.DayNumber {
found = true
if value > day.High {
day.High = value
}
if value < day.Low || day.Low == 0 {
day.Low = value
}
days[key] = day
}
}
if !found {
days = append(days, DayData{
DayNumber: dayNo,
DayTime: dayStart(historyChange.LastUpdated.Local()),
Value: value,
})
}
}
}
for key, day := range days {
day.Value = day.High - day.Low
days[key] = day
}
days = fillMissing(days, startTime, endTime)
return days, nil
}
func fillMissing(days []DayData, startTime, endTime time.Time) []DayData {
var (
previousDay time.Time
defaultDay time.Time
previousValue float32
ret []DayData
currentTime = time.Now()
)
expectedDaysDiff := int(math.Trunc(endTime.Sub(startTime).Hours()/24) + 1)
for key, day := range days {
if key != 0 {
if day.DayTime.Day() != previousDay.Add(24*time.Hour).Day() {
daysDiff := math.Trunc(day.DayTime.Sub(previousDay).Hours() / 24)
for i := 1; float64(i) < daysDiff; i++ {
fakeTime := previousDay.Add(time.Duration(24*i) * time.Hour)
ret = append(ret, DayData{
DayNumber: fakeTime.Day(),
DayTime: dayStart(fakeTime),
Value: previousValue,
})
}
}
}
ret = append(ret, day)
previousValue = day.Value
previousDay = day.DayTime
}
// note that here previousDay is the last logged day
if previousDay == defaultDay {
return []DayData{}
}
if previousDay.Day() != currentTime.Day() {
daysDiff := math.Trunc(currentTime.Sub(previousDay).Hours() / 24)
for i := 1; float64(i) < daysDiff; i++ {
fakeTime := previousDay.Add(time.Duration(24*i) * time.Hour)
ret = append(ret, DayData{
DayNumber: fakeTime.Day(),
DayTime: dayStart(fakeTime),
Value: previousValue,
})
}
}
if len(ret) < expectedDaysDiff {
shouldAdd := expectedDaysDiff - len(ret)
startDay := currentTime.Add(-time.Duration(24*len(ret)) * time.Hour)
for i := 0; i < shouldAdd; i++ {
fakeTime := startDay.Add(-time.Duration(24*i) * time.Hour)
ret = append([]DayData{
{
DayNumber: fakeTime.Day(),
DayTime: dayStart(fakeTime),
Value: 0,
},
}, ret...)
}
}
if len(ret) != expectedDaysDiff {
// oh shit
log.Panicln("You've found a logic bug! Open a bug report ASAP.")
}
return ret
}