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 }