forked from massivebox/ecodash
MassiveBox
52ba0ea4c1
- 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.
318 lines
6.9 KiB
Go
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
|
|
|
|
}
|