diff --git a/cmd/main.go b/cmd/main.go index bb57977..e4f6b46 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -52,7 +52,8 @@ func main() { solar := internal.NewSolar(client, deyeUser, deyePassword) myCron := cron.New() - myCron.AddFunc("0 0 18 * * *", solar.SendDailyReport) + myCron.AddFunc("0 0 18 * * *", solar.CronSendDailyReport) + myCron.AddFunc("0 */15 * * * *", solar.CronCollectMetrics) myCron.Start() // Listen to Ctrl+C (you can also do something else that prevents the program from exiting) diff --git a/internal/solar.go b/internal/solar.go index 8b292c7..6355b62 100644 --- a/internal/solar.go +++ b/internal/solar.go @@ -12,15 +12,23 @@ import ( const adminStatusEndpoint = "http://deye-solar-inverter/status.html" const expectedDailyOutput float64 = 20 +const criticalBelowDailyOutput float64 = 10 var yieldTodayPattern = regexp.MustCompile(`var webdata_today_e = "([^"]+)";`) var yieldTotalPattern = regexp.MustCompile(`var webdata_total_e = "([^"]+)";`) +type SolarMetrics struct { + lastUpdatedAt time.Time + today float64 + total float64 +} + type Solar struct { client *Client channelId string adminUser string adminPassword string + metrics SolarMetrics } func NewSolar(client *Client, adminUser, adminPassword string) *Solar { @@ -38,39 +46,36 @@ func NewSolar(client *Client, adminUser, adminPassword string) *Solar { } } -func (this *Solar) SendDailyReport() { - metrics, err := this.querySolarMetrics() +func (this *Solar) CronSendDailyReport() { + // TODO: Add retries? + fmt.Println("[SOLAR] Cron job invoked to send daily report") + defer fmt.Println("[SOLAR] Cron job completed to send daily report") + + if this.metrics.lastUpdatedAt.IsZero() { + fmt.Println("[SOLAR] Got zero value for lastUpdatedAt, means either inverter is not working properly or there was a long power outage or network connectivity problems") + + this.client.SendTextMessage( + this.channelId, + messageHeaderLine()+ + "🔴 Couldn't collect any data today. Check logs, inverter and connectivity.", + ) - if err != nil { - fmt.Println("Error querying soalr metrics:", err) return } - fmt.Printf("[SOLAR] Metrics: %v\n", metrics) - - deltaYieldToday := metrics.today - expectedDailyOutput - - var summary string - - if deltaYieldToday < 0 { - summary = fmt.Sprintf("â˜šī¸ Below expected daily yield of %.2f kWh", expectedDailyOutput) - } else { - summary = fmt.Sprintf("😁 Above expected daily yield by %.2f kWh", deltaYieldToday) - } - - var todayFormatted = time.Now().Format("Monday, 2 Jan 2006") + fmt.Printf("[SOLAR] Metrics: %v\n", this.metrics) message := fmt.Sprintf( - "*🔆 Solar Production Report — %s*\n\n"+ - summary+ + messageHeaderLine()+ + this.getSummary()+ "\n\n"+ "*Today: %.2f kWh*\n"+ "Total: %.2f kWh\n\n"+ - "`DO NOT CLICK: %d`", - todayFormatted, - metrics.today, - metrics.total, - time.Now().Unix(), + "_Last updated: %s_\n\n"+ + messageDoNotClickLine(), + this.metrics.today, + this.metrics.total, + time.Now().Format("03:04 PM, 2006-01-02"), ) fmt.Printf("[SOLAR] Message to be sent:\n%s", message) @@ -81,10 +86,40 @@ func (this *Solar) SendDailyReport() { ) } -func (this *Solar) querySolarMetrics() (*struct { - today float64 - total float64 -}, error) { +func (this *Solar) getSummary() string { + deltaYieldToday := this.metrics.today - expectedDailyOutput + + if this.metrics.today < criticalBelowDailyOutput { + return fmt.Sprintf("âš ī¸ Yield was only %.2f kWh today, well below expected %.2f kWh. Check if this was due to bad weather/powercut or if panels need maintenance.", this.metrics.today, expectedDailyOutput) + } else if deltaYieldToday < 0 { + return fmt.Sprintf("â˜šī¸ Below expected daily yield of %.2f kWh", expectedDailyOutput) + } else { + return fmt.Sprintf("😁 Above expected daily yield by %.2f kWh", deltaYieldToday) + } +} + +func (this *Solar) CronCollectMetrics() { + fmt.Println("[SOLAR] Cron job invoked to collect fresh metrics") + defer fmt.Println("[SOLAR] Cron job completed to collect fresh metrics") + + // Reset if we rolled over to next day + if !AreSameDay(time.Now(), this.metrics.lastUpdatedAt) { + this.metrics = SolarMetrics{} + } + + metrics, err := this.querySolarMetrics() + + if err != nil { + fmt.Println("[SOLAR] Error querying solar metrics:", err) + return + } + + this.metrics = *metrics + + fmt.Printf("[SOLAR] Updated metrics: %+v\n", this.metrics) +} + +func (this *Solar) querySolarMetrics() (*SolarMetrics, error) { req, err := http.NewRequest("GET", adminStatusEndpoint, nil) if err != nil { panic(err) @@ -92,7 +127,6 @@ func (this *Solar) querySolarMetrics() (*struct { req.SetBasicAuth(this.adminUser, this.adminPassword) res, err := http.DefaultClient.Do(req) - if err != nil { panic(err) } @@ -104,7 +138,6 @@ func (this *Solar) querySolarMetrics() (*struct { defer res.Body.Close() rawBody, err := io.ReadAll(res.Body) - if err != nil { return nil, err } @@ -117,7 +150,6 @@ func (this *Solar) querySolarMetrics() (*struct { } yieldToday, err := strconv.ParseFloat(matches[1], 64) - if err != nil { return nil, fmt.Errorf("Failed to convert %s to float64: %w", matches[1], err) } @@ -129,16 +161,21 @@ func (this *Solar) querySolarMetrics() (*struct { } yieldTotal, err := strconv.ParseFloat(matches[1], 64) - if err != nil { return nil, fmt.Errorf("Failed to convert %s to float64: %w", matches[1], err) } - return &struct { - today float64 - total float64 - }{ - today: yieldToday, - total: yieldTotal, + return &SolarMetrics{ + lastUpdatedAt: time.Now(), + today: yieldToday, + total: yieldTotal, }, nil } + +func messageDoNotClickLine() string { + return fmt.Sprintf("`DO NOT CLICK: %d`", time.Now().Unix()) +} + +func messageHeaderLine() string { + return fmt.Sprintf("*🔆 Solar Production Report — %s*\n\n", time.Now().Format("2 Jan 2006")) +}