From 9ed6c8cd99377e445e28b4f9277e121780e1dc7f Mon Sep 17 00:00:00 2001 From: Sangeeth Sudheer Date: Sat, 12 Apr 2025 16:57:19 +0530 Subject: [PATCH] Add solar usage reporting --- .gitignore | 5 +- Makefile | 2 +- cmd/main.go | 15 +++++ go.mod | 5 +- go.sum | 2 + internal/client.go | 25 +++++++- internal/solar.go | 144 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 internal/solar.go diff --git a/.gitignore b/.gitignore index cbb0dbf..8232107 100644 --- a/.gitignore +++ b/.gitignore @@ -93,7 +93,7 @@ Temporary Items # End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,macos,linux -## +## ## ## Custom ## @@ -101,4 +101,5 @@ Temporary Items whatsapp.db auto-response-time-map.json -out/ \ No newline at end of file +out/ +.env diff --git a/Makefile b/Makefile index 25d767f..f12cd40 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ start: install-deps - go run cmd/main.go + source .env && go run cmd/main.go install-deps: go mod tidy diff --git a/cmd/main.go b/cmd/main.go index c599991..c34c577 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,6 +6,7 @@ import ( "os/signal" "syscall" + "github.com/robfig/cron" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/types" @@ -41,10 +42,24 @@ func main() { client.Register() client.Connect() + deyeUser := os.Getenv("DEYE_USER") + deyePassword := os.Getenv("DEYE_PASSWORD") + + if deyeUser == "" || deyePassword == "" { + panic("Deye user/password must be provided") + } + + solar := internal.NewSolar(client, deyeUser, deyePassword) + + myCron := cron.New() + myCron.AddFunc("0 0 19 * * *", solar.SendDailyReport) + myCron.Start() + // Listen to Ctrl+C (you can also do something else that prevents the program from exiting) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) <-c + myCron.Stop() client.Disconnect() } diff --git a/go.mod b/go.mod index c21ce29..aac4a15 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module git.sangeeth.dev/wa-autoresponder go 1.24.1 -require go.mau.fi/whatsmeow v0.0.0-20250326122532-6680c9a6e9a7 +require ( + github.com/robfig/cron v1.2.0 + go.mau.fi/whatsmeow v0.0.0-20250326122532-6680c9a6e9a7 +) require rsc.io/qr v0.2.0 // indirect diff --git a/go.sum b/go.sum index 9488b46..68488e8 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxO github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= diff --git a/internal/client.go b/internal/client.go index ee56981..b661253 100644 --- a/internal/client.go +++ b/internal/client.go @@ -178,7 +178,7 @@ func (client *Client) eventHandler(evt interface{}) { time.Sleep(2 * time.Duration(rand.IntN(3)) * time.Second) - msg := proto.String(client.message + "\n\nIgnore this random number: `" + strconv.FormatInt(time.Now().UnixMilli(), 10) + "`") + msg := proto.String(client.message + "\n\nIgnore this random number: `" + strconv.FormatInt(time.Now().Unix(), 10) + "`") imageResp, err := client.WAClient.Upload(context.Background(), waautoresponder.Bernie, whatsmeow.MediaImage) @@ -216,3 +216,26 @@ func (client *Client) eventHandler(evt interface{}) { client.updateAutoResponseTime(chatUserId) } } + +func (client *Client) SendTextMessage(chatId string, message string) { + chatJID, err := types.ParseJID(chatId) + + if err != nil { + fmt.Println("Error parsing JID:", err) + return + } + + time.Sleep(2 * time.Duration(rand.IntN(3)) * time.Second) + client.WAClient.SendChatPresence(chatJID, types.ChatPresenceComposing, types.ChatPresenceMediaText) + time.Sleep(2 * time.Duration(rand.IntN(3)) * time.Second) + + client.WAClient.SendMessage( + context.Background(), + chatJID, + &waE2E.Message{ + Conversation: proto.String(message), + }, + ) + + client.WAClient.SendChatPresence(chatJID, types.ChatPresencePaused, types.ChatPresenceMediaText) +} diff --git a/internal/solar.go b/internal/solar.go new file mode 100644 index 0000000..47a3e6a --- /dev/null +++ b/internal/solar.go @@ -0,0 +1,144 @@ +package internal + +import ( + "fmt" + "io" + "net/http" + "os" + "regexp" + "strconv" + "time" +) + +const adminStatusEndpoint = "http://deye-solar-inverter/status.html" +const expectedDailyOutput float64 = 20 + +var yieldTodayPattern = regexp.MustCompile(`var webdata_today_e = "([^"]+)";`) +var yieldTotalPattern = regexp.MustCompile(`var webdata_total_e = "([^"]+)";`) + +type Solar struct { + client *Client + channelId string + adminUser string + adminPassword string +} + +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) SendDailyReport() { + metrics, err := this.querySolarMetrics() + + 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") + + message := fmt.Sprintf( + "*🔆 Solar Production Report, %s*\n\n"+ + summary+ + "\n\n"+ + "Today: %.2f kWh\n"+ + "Total: %.2f kWh\n\n"+ + "`DO NOT CLICK: %d`", + todayFormatted, + metrics.today, + metrics.total, + time.Now().Unix(), + ) + + fmt.Printf("[SOLAR] Message to be sent:\n%s", message) + + this.client.SendTextMessage( + this.channelId, + message, + ) +} + +func (this *Solar) querySolarMetrics() (*struct { + today float64 + total float64 +}, 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 &struct { + today float64 + total float64 + }{ + today: yieldToday, + total: yieldTotal, + }, nil +}