Add solar usage reporting
This commit is contained in:
parent
3cd448d9ee
commit
9ed6c8cd99
5
.gitignore
vendored
5
.gitignore
vendored
@ -93,7 +93,7 @@ Temporary Items
|
|||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,macos,linux
|
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,macos,linux
|
||||||
|
|
||||||
##
|
##
|
||||||
##
|
##
|
||||||
## Custom
|
## Custom
|
||||||
##
|
##
|
||||||
@ -101,4 +101,5 @@ Temporary Items
|
|||||||
|
|
||||||
whatsapp.db
|
whatsapp.db
|
||||||
auto-response-time-map.json
|
auto-response-time-map.json
|
||||||
out/
|
out/
|
||||||
|
.env
|
||||||
|
2
Makefile
2
Makefile
@ -1,5 +1,5 @@
|
|||||||
start: install-deps
|
start: install-deps
|
||||||
go run cmd/main.go
|
source .env && go run cmd/main.go
|
||||||
|
|
||||||
install-deps:
|
install-deps:
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
15
cmd/main.go
15
cmd/main.go
@ -6,6 +6,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/robfig/cron"
|
||||||
"go.mau.fi/whatsmeow"
|
"go.mau.fi/whatsmeow"
|
||||||
"go.mau.fi/whatsmeow/store/sqlstore"
|
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||||
"go.mau.fi/whatsmeow/types"
|
"go.mau.fi/whatsmeow/types"
|
||||||
@ -41,10 +42,24 @@ func main() {
|
|||||||
client.Register()
|
client.Register()
|
||||||
client.Connect()
|
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)
|
// Listen to Ctrl+C (you can also do something else that prevents the program from exiting)
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
<-c
|
<-c
|
||||||
|
|
||||||
|
myCron.Stop()
|
||||||
client.Disconnect()
|
client.Disconnect()
|
||||||
}
|
}
|
||||||
|
5
go.mod
5
go.mod
@ -2,7 +2,10 @@ module git.sangeeth.dev/wa-autoresponder
|
|||||||
|
|
||||||
go 1.24.1
|
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
|
require rsc.io/qr v0.2.0 // indirect
|
||||||
|
|
||||||
|
2
go.sum
2
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/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 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
@ -178,7 +178,7 @@ func (client *Client) eventHandler(evt interface{}) {
|
|||||||
|
|
||||||
time.Sleep(2 * time.Duration(rand.IntN(3)) * time.Second)
|
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)
|
imageResp, err := client.WAClient.Upload(context.Background(), waautoresponder.Bernie, whatsmeow.MediaImage)
|
||||||
|
|
||||||
@ -216,3 +216,26 @@ func (client *Client) eventHandler(evt interface{}) {
|
|||||||
client.updateAutoResponseTime(chatUserId)
|
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
144
internal/solar.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user