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)
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)

View File

@ -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"))
}