182 lines
4.5 KiB
Go
182 lines
4.5 KiB
Go
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
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 {
|
|
channelId := os.Getenv("SOLAR_REPORT_CHANNEL_ID")
|
|
|
|
if channelId == "" {
|
|
panic("SOLAR_REPORT_CHANNEL_ID must be set")
|
|
}
|
|
|
|
return &Solar{
|
|
client: client,
|
|
adminUser: adminUser,
|
|
adminPassword: adminPassword,
|
|
channelId: channelId,
|
|
}
|
|
}
|
|
|
|
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.",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
fmt.Printf("[SOLAR] Metrics: %v\n", this.metrics)
|
|
|
|
message := fmt.Sprintf(
|
|
messageHeaderLine()+
|
|
this.getSummary()+
|
|
"\n\n"+
|
|
"*Today: %.2f kWh*\n"+
|
|
"Total: %.2f kWh\n\n"+
|
|
"_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)
|
|
|
|
this.client.SendTextMessage(
|
|
this.channelId,
|
|
message,
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
req.SetBasicAuth(this.adminUser, this.adminPassword)
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("Bad status %d querying status page", res.StatusCode)
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
rawBody, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
html := string(rawBody)
|
|
matches := yieldTodayPattern.FindStringSubmatch(html)
|
|
|
|
if len(matches) != 2 {
|
|
return nil, fmt.Errorf("Regex failed to match today's yield")
|
|
}
|
|
|
|
yieldToday, err := strconv.ParseFloat(matches[1], 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to convert %s to float64: %w", matches[1], err)
|
|
}
|
|
|
|
matches = yieldTotalPattern.FindStringSubmatch(html)
|
|
|
|
if len(matches) != 2 {
|
|
return nil, fmt.Errorf("Regex failed to match total yield")
|
|
}
|
|
|
|
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 &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"))
|
|
}
|