solar: Collect metrics periodically

This commit is contained in:
Sangeeth Sudheer 2025-04-12 19:37:33 +05:30
parent 7627d48aaf
commit ae1c064e52
Signed by: x
GPG Key ID: F6D06ECE734C57D1
2 changed files with 77 additions and 39 deletions

View File

@ -52,7 +52,8 @@ func main() {
solar := internal.NewSolar(client, deyeUser, deyePassword) solar := internal.NewSolar(client, deyeUser, deyePassword)
myCron := cron.New() 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() myCron.Start()
// Listen to Ctrl+C (you can also do something else that prevents the program from exiting) // Listen to Ctrl+C (you can also do something else that prevents the program from exiting)

View File

@ -12,15 +12,23 @@ import (
const adminStatusEndpoint = "http://deye-solar-inverter/status.html" const adminStatusEndpoint = "http://deye-solar-inverter/status.html"
const expectedDailyOutput float64 = 20 const expectedDailyOutput float64 = 20
const criticalBelowDailyOutput float64 = 10
var yieldTodayPattern = regexp.MustCompile(`var webdata_today_e = "([^"]+)";`) var yieldTodayPattern = regexp.MustCompile(`var webdata_today_e = "([^"]+)";`)
var yieldTotalPattern = regexp.MustCompile(`var webdata_total_e = "([^"]+)";`) var yieldTotalPattern = regexp.MustCompile(`var webdata_total_e = "([^"]+)";`)
type SolarMetrics struct {
lastUpdatedAt time.Time
today float64
total float64
}
type Solar struct { type Solar struct {
client *Client client *Client
channelId string channelId string
adminUser string adminUser string
adminPassword string adminPassword string
metrics SolarMetrics
} }
func NewSolar(client *Client, adminUser, adminPassword string) *Solar { func NewSolar(client *Client, adminUser, adminPassword string) *Solar {
@ -38,39 +46,36 @@ func NewSolar(client *Client, adminUser, adminPassword string) *Solar {
} }
} }
func (this *Solar) SendDailyReport() { func (this *Solar) CronSendDailyReport() {
metrics, err := this.querySolarMetrics() // 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 return
} }
fmt.Printf("[SOLAR] Metrics: %v\n", metrics) fmt.Printf("[SOLAR] Metrics: %v\n", this.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")
message := fmt.Sprintf( message := fmt.Sprintf(
"*🔆 Solar Production Report — %s*\n\n"+ messageHeaderLine()+
summary+ this.getSummary()+
"\n\n"+ "\n\n"+
"*Today: %.2f kWh*\n"+ "*Today: %.2f kWh*\n"+
"Total: %.2f kWh\n\n"+ "Total: %.2f kWh\n\n"+
"`DO NOT CLICK: %d`", "_Last updated: %s_\n\n"+
todayFormatted, messageDoNotClickLine(),
metrics.today, this.metrics.today,
metrics.total, this.metrics.total,
time.Now().Unix(), time.Now().Format("03:04 PM, 2006-01-02"),
) )
fmt.Printf("[SOLAR] Message to be sent:\n%s", message) fmt.Printf("[SOLAR] Message to be sent:\n%s", message)
@ -81,10 +86,40 @@ func (this *Solar) SendDailyReport() {
) )
} }
func (this *Solar) querySolarMetrics() (*struct { func (this *Solar) getSummary() string {
today float64 deltaYieldToday := this.metrics.today - expectedDailyOutput
total float64
}, error) { 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) req, err := http.NewRequest("GET", adminStatusEndpoint, nil)
if err != nil { if err != nil {
panic(err) panic(err)
@ -92,7 +127,6 @@ func (this *Solar) querySolarMetrics() (*struct {
req.SetBasicAuth(this.adminUser, this.adminPassword) req.SetBasicAuth(this.adminUser, this.adminPassword)
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -104,7 +138,6 @@ func (this *Solar) querySolarMetrics() (*struct {
defer res.Body.Close() defer res.Body.Close()
rawBody, err := io.ReadAll(res.Body) rawBody, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -117,7 +150,6 @@ func (this *Solar) querySolarMetrics() (*struct {
} }
yieldToday, err := strconv.ParseFloat(matches[1], 64) yieldToday, err := strconv.ParseFloat(matches[1], 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to convert %s to float64: %w", matches[1], err) 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) yieldTotal, err := strconv.ParseFloat(matches[1], 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to convert %s to float64: %w", matches[1], err) return nil, fmt.Errorf("Failed to convert %s to float64: %w", matches[1], err)
} }
return &struct { return &SolarMetrics{
today float64 lastUpdatedAt: time.Now(),
total float64 today: yieldToday,
}{ total: yieldTotal,
today: yieldToday,
total: yieldTotal,
}, nil }, 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"))
}