forked from massivebox/ecodash
MassiveBox
7a1214d492
- The program will now be cross-compiled and released for arm as well as x86 - What we previously called "cache" is not actually a cache, as it holds content that can't be always retrieved. Now we're stopping calling it a cache. - Improved history merging - Fixed the README to include the database as a volume, and to fix some errors
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 := dayStart(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 := dayStart(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
|
|
|
|
}
|