Skip to content

Commit e6c3a42

Browse files
author
Ted Pearson
committed
add csv parsing; separate browser functions
1 parent 099f0d3 commit e6c3a42

File tree

5 files changed

+159
-57
lines changed

5 files changed

+159
-57
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ module electric-usage-downloader
33
go 1.19
44

55
require (
6+
github.com/chromedp/cdproto v0.0.0-20220924210414-0e3390be1777
67
github.com/chromedp/chromedp v0.8.6
8+
github.com/shopspring/decimal v1.3.1
79
gopkg.in/yaml.v3 v3.0.1
810
)
911

1012
require (
11-
github.com/chromedp/cdproto v0.0.0-20220924210414-0e3390be1777 // indirect
1213
github.com/chromedp/sysutil v1.0.0 // indirect
1314
github.com/gobwas/httphead v0.1.0 // indirect
1415
github.com/gobwas/pool v0.2.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL
1616
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
1717
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
1818
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
19+
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
20+
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
1921
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
2022
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
2123
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

internal/app/browser.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package app
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"time"
8+
9+
"github.com/chromedp/cdproto/browser"
10+
"github.com/chromedp/chromedp"
11+
)
12+
13+
type Config struct {
14+
Username string
15+
Password string
16+
LoginUrl string
17+
DownloadDir string
18+
}
19+
20+
func DownloadCsv(config *Config, startDate string, endDate string) (string, error) {
21+
allocatorFlags := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.Flag("headless", false))
22+
ctx, cancel := chromedp.NewExecAllocator(context.Background(), allocatorFlags...)
23+
defer cancel()
24+
ctx, cancel = chromedp.NewContext(ctx, chromedp.WithLogf(log.Printf))
25+
defer cancel()
26+
ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
27+
defer cancel()
28+
29+
done := make(chan string, 1)
30+
chromedp.ListenTarget(ctx, func(ev interface{}) {
31+
if ev, ok := ev.(*browser.EventDownloadProgress); ok {
32+
if ev.TotalBytes != 0 {
33+
//fmt.Printf("State: %s, completed: %.2f, total: %.2f\n", ev.State.String(), ev.ReceivedBytes, ev.TotalBytes)
34+
if ev.State == browser.DownloadProgressStateCompleted {
35+
done <- ev.GUID
36+
close(done)
37+
}
38+
}
39+
}
40+
})
41+
42+
err := chromedp.Run(ctx,
43+
chromedp.Navigate(config.LoginUrl),
44+
chromedp.SetValue("#LoginUsernameTextBox", config.Username, chromedp.NodeVisible),
45+
chromedp.SetValue("#LoginPasswordTextBox", config.Password),
46+
chromedp.Click("#LoginSubmitButton"),
47+
browser.SetDownloadBehavior(browser.SetDownloadBehaviorBehaviorAllowAndName).
48+
WithDownloadPath(config.DownloadDir).
49+
WithEventsEnabled(true),
50+
chromedp.Click("#MyUsageDropDown > a", chromedp.NodeVisible),
51+
chromedp.Click(`//div[.="Usage Explorer"]`, chromedp.NodeVisible),
52+
chromedp.Click(`//img[@alt='Usage Management']`, chromedp.NodeVisible),
53+
chromedp.Sleep(time.Second),
54+
chromedp.Click(`(//input[@name="timeFrameRadio"])[3]`, chromedp.NodeVisible),
55+
chromedp.SetValue(`(//input[contains(@class, "form-control-readonly")])[1]`, startDate),
56+
chromedp.SetValue(`(//input[contains(@class, "form-control-readonly")])[2]`, endDate),
57+
chromedp.Click(`(//input[@name="fileFormatRadio"])[2]`),
58+
chromedp.Click(`//button[.="Download Usage Data"]`),
59+
)
60+
if err != nil {
61+
return "", err
62+
}
63+
guid := <-done
64+
return fmt.Sprintf("%s/%s", config.DownloadDir, guid), nil
65+
}

internal/app/parser.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package app
2+
3+
import (
4+
"bufio"
5+
"log"
6+
"os"
7+
"regexp"
8+
"strings"
9+
"time"
10+
11+
"github.com/shopspring/decimal"
12+
)
13+
14+
type ElectricUsage struct {
15+
StartTime time.Time
16+
EndTime time.Time
17+
WattHours int64
18+
CostInCents int64
19+
}
20+
21+
type ElectricRecords struct {
22+
ElectricUsage []*ElectricUsage
23+
}
24+
25+
func ParseCsv(file string) (*ElectricRecords, error) {
26+
dateRegex, err := regexp.Compile(`(\d{4}-\d\d-\d\d \d\d:\d\d) to (\d{4}-\d\d-\d\d \d\d:\d\d)`)
27+
if err != nil {
28+
log.Fatal(err)
29+
}
30+
// drop all lines until it starts with dddd-dd-dd
31+
data, err := os.Open(file)
32+
if err != nil {
33+
return nil, err
34+
}
35+
defer func() {
36+
if err := data.Close(); err != nil {
37+
panic(err)
38+
}
39+
}()
40+
scanner := bufio.NewScanner(data)
41+
42+
records := make([]*ElectricUsage, 0, 100)
43+
for scanner.Scan() {
44+
line := scanner.Text()
45+
matches := dateRegex.FindStringSubmatch(line)
46+
if matches != nil {
47+
cols := strings.Split(line, ",")
48+
usage := parseRecord(cols, matches[1:3])
49+
records = append(records, usage)
50+
}
51+
}
52+
return &ElectricRecords{records}, nil
53+
}
54+
55+
func parseRecord(row []string, dates []string) *ElectricUsage {
56+
if len(row) < 3 {
57+
log.Printf("Bad record: %s\n", row)
58+
return nil
59+
}
60+
startTime, err1 := time.ParseInLocation("2006-01-02 15:04", dates[0], time.Local)
61+
endTime, err2 := time.ParseInLocation("2006-01-02 15:04", dates[1], time.Local)
62+
kilowattHours, err3 := decimal.NewFromString(row[1])
63+
cents, err4 := decimal.NewFromString(row[2])
64+
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
65+
log.Printf("Bad record: %s\n", row)
66+
return nil
67+
}
68+
return &ElectricUsage{
69+
StartTime: startTime,
70+
EndTime: endTime,
71+
WattHours: kilowattHours.Mul(decimal.NewFromInt(1000)).IntPart(),
72+
CostInCents: cents.Mul(decimal.NewFromInt(100)).IntPart(),
73+
}
74+
}

main.go

Lines changed: 16 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,41 @@
11
package main
22

33
import (
4-
"context"
54
"fmt"
6-
"github.com/chromedp/cdproto/browser"
7-
"github.com/chromedp/chromedp"
8-
"gopkg.in/yaml.v3"
95
"log"
106
"os"
11-
)
127

13-
const loginUrl string = "https://novec.smarthub.coop/Login.html"
8+
"gopkg.in/yaml.v3"
9+
10+
"electric-usage-downloader/internal/app"
11+
)
1412

1513
var (
1614
startDate = "09/29/2022"
1715
endDate = "09/30/2022"
1816
)
1917

20-
type Config struct {
21-
Username string
22-
Password string
23-
}
24-
2518
func main() {
26-
2719
// read config
2820
file, err := os.ReadFile("config.yaml")
2921
if err != nil {
3022
log.Fatal(err)
3123
}
32-
config := &Config{}
24+
config := &app.Config{}
3325
err = yaml.Unmarshal(file, config)
3426
if err != nil {
3527
log.Fatal(err)
3628
}
3729

38-
ctx, cancel := chromedp.NewExecAllocator(context.Background(), append(chromedp.DefaultExecAllocatorOptions[:], chromedp.Flag("headless", false))...)
39-
defer cancel()
40-
41-
ctx, cancel = chromedp.NewContext(ctx, chromedp.WithLogf(log.Printf))
42-
defer cancel()
43-
44-
//ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
45-
//defer cancel()
46-
47-
done := make(chan string, 1)
48-
chromedp.ListenTarget(ctx, func(ev interface{}) {
49-
if ev, ok := ev.(*browser.EventDownloadProgress); ok {
50-
if ev.TotalBytes != 0 {
51-
fmt.Printf("State: %s, completed: %.2f, total: %.2f\n", ev.State.String(), ev.ReceivedBytes, ev.TotalBytes)
52-
if ev.State == browser.DownloadProgressStateCompleted {
53-
done <- ev.GUID
54-
close(done)
55-
}
56-
}
57-
}
58-
})
59-
60-
chromedp.Run(ctx,
61-
chromedp.Navigate(loginUrl),
62-
//chromedp.WaitVisible("#LoginUsernameTextBox"),
63-
chromedp.SetValue("#LoginUsernameTextBox", config.Username, chromedp.NodeVisible),
64-
chromedp.SetValue("#LoginPasswordTextBox", config.Password),
65-
chromedp.Click("#LoginSubmitButton"),
66-
//chromedp.WaitVisible(`//a[. = "Log Out"]`),
67-
browser.SetDownloadBehavior(browser.SetDownloadBehaviorBehaviorAllowAndName).
68-
WithDownloadPath("/tmp").
69-
WithEventsEnabled(true),
70-
chromedp.Click("#MyUsageDropDown > a", chromedp.NodeVisible),
71-
chromedp.Click(`//div[.="Usage Explorer"]`, chromedp.NodeVisible),
72-
chromedp.Click(`//img[@alt='Usage Management']`, chromedp.NodeVisible),
73-
chromedp.Click(`(//input[@name="timeFrameRadio"])[3]`, chromedp.NodeVisible),
74-
chromedp.SetValue(`(//input[contains(@class, "form-control-readonly")])[1]`, startDate),
75-
chromedp.SetValue(`(//input[contains(@class, "form-control-readonly")])[2]`, endDate),
76-
chromedp.Click(`(//input[@name="fileFormatRadio"])[2]`),
77-
chromedp.Click(`//button[.="Download Usage Data"]`),
78-
)
79-
guid := <-done
80-
fmt.Printf("hello %s", guid)
30+
path, err := app.DownloadCsv(config, startDate, endDate)
31+
if err != nil {
32+
log.Fatal(err)
33+
}
34+
fmt.Printf("file downloaded: %s", path)
35+
// parse csv!
36+
records, err := app.ParseCsv(path)
37+
if err != nil {
38+
log.Fatal(err)
39+
}
40+
fmt.Printf("%+v", records)
8141
}

0 commit comments

Comments
 (0)