Turns out HomeAssistant only returns 10 days'data byault. This is a problem that we will havsoon. Now the cache doesn't reset eh time it only if some data is missing.
318 lines
6.9 KiB
Go
Executable file
318 lines
6.9 KiB
Go
Executable file
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
|
|
|
|
}
|