Add solar usage reporting

This commit is contained in:
Sangeeth Sudheer 2025-04-12 16:57:19 +05:30
parent 3cd448d9ee
commit 9ed6c8cd99
Signed by: x
GPG Key ID: F6D06ECE734C57D1
7 changed files with 193 additions and 5 deletions

1
.gitignore vendored
View File

@ -102,3 +102,4 @@ Temporary Items
whatsapp.db
auto-response-time-map.json
out/
.env

View File

@ -1,5 +1,5 @@
start: install-deps
go run cmd/main.go
source .env && go run cmd/main.go
install-deps:
go mod tidy

View File

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

5
go.mod
View File

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

2
go.sum
View File

@ -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=

View File

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

144
internal/solar.go Normal file
View File

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