Skip to content

Commit

Permalink
Implement basic hardware detection (evcc-io#435)
Browse files Browse the repository at this point in the history
andig authored Nov 21, 2020

Unverified

The committer email address is not verified.
1 parent f093cc5 commit 068fba5
Showing 23 changed files with 1,519 additions and 78 deletions.
34 changes: 9 additions & 25 deletions charger/keba.go
Original file line number Diff line number Diff line change
@@ -4,10 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"reflect"
"strings"
"time"

"github.com/andig/evcc/api"
@@ -19,7 +16,6 @@ import (

const (
udpTimeout = time.Second
kebaPort = 7090
)

// RFID contains access credentials
@@ -34,6 +30,7 @@ type Keba struct {
rfid RFID
timeout time.Duration
recv chan keba.UDPMsg
sender *keba.Sender
}

func init() {
@@ -58,51 +55,38 @@ func NewKebaFromConfig(other map[string]interface{}) (api.Charger, error) {
}

// NewKeba creates a new charger
func NewKeba(conn, serial string, rfid RFID, timeout time.Duration) (api.Charger, error) {
func NewKeba(uri, serial string, rfid RFID, timeout time.Duration) (api.Charger, error) {
log := util.NewLogger("keba")

var err error
if keba.Instance == nil {
keba.Instance, err = keba.New(log, fmt.Sprintf(":%d", kebaPort))
keba.Instance, err = keba.New(log)
if err != nil {
return nil, err
}
}

// add default port
conn = util.DefaultPort(conn, kebaPort)
conn := util.DefaultPort(uri, keba.Port)
sender, err := keba.NewSender(uri)

c := &Keba{
log: log,
conn: conn,
rfid: rfid,
timeout: timeout,
recv: make(chan keba.UDPMsg),
sender: sender,
}

// use serial to subscribe if defined for docker scenarios
if serial == "" {
serial = conn
}

return c, keba.Instance.Subscribe(serial, c.recv)
}

func (c *Keba) send(msg string) error {
raddr, err := net.ResolveUDPAddr("udp", c.conn)
if err != nil {
return err
}

conn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
return err
}

defer conn.Close()
keba.Instance.Subscribe(serial, c.recv)

_, err = io.Copy(conn, strings.NewReader(msg))
return err
return c, err
}

func (c *Keba) receive(report int, resC chan<- keba.UDPMsg, errC chan<- error, closeC <-chan struct{}) {
@@ -140,7 +124,7 @@ func (c *Keba) roundtrip(msg string, report int, res interface{}) error {

go c.receive(report, resC, errC, closeC)

if err := c.send(msg); err != nil {
if err := c.sender.Send(msg); err != nil {
return err
}

23 changes: 13 additions & 10 deletions charger/keba/listener.go
Original file line number Diff line number Diff line change
@@ -13,8 +13,14 @@ import (
const (
udpBufferSize = 1024

// Port is the KEBA UDP port
Port = 7090

// OK is the KEBA confirmation message
OK = "TCH-OK :done"

// Any subscriber receives all messages
Any = "<any>"
)

// Instance is the KEBA listener instance
@@ -37,8 +43,8 @@ type Listener struct {
}

// New creates a UDP listener that clients can subscribe to
func New(log *util.Logger, addr string) (*Listener, error) {
laddr, err := net.ResolveUDPAddr("udp", addr)
func New(log *util.Logger) (*Listener, error) {
laddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", Port))
if err != nil {
return nil, err
}
@@ -60,16 +66,11 @@ func New(log *util.Logger, addr string) (*Listener, error) {
}

// Subscribe adds a client address and message channel
func (l *Listener) Subscribe(addr string, c chan<- UDPMsg) error {
func (l *Listener) Subscribe(addr string, c chan<- UDPMsg) {
l.mux.Lock()
defer l.mux.Unlock()

if _, exists := l.clients[addr]; exists {
return fmt.Errorf("duplicate subscription: %s", addr)
}

l.clients[addr] = c
return nil
}

func (l *Listener) listen() {
@@ -78,7 +79,7 @@ func (l *Listener) listen() {
for {
read, addr, err := l.conn.ReadFrom(b)
if err != nil {
l.log.ERROR.Printf("listener: %v", err)
l.log.TRACE.Printf("listener: %v", err)
continue
}

@@ -107,6 +108,8 @@ func (l *Listener) listen() {
// addrMatches checks if either message sender or serial matched given addr
func (l *Listener) addrMatches(addr string, msg UDPMsg) bool {
switch {
case addr == Any:
return true
case addr == msg.Addr:
return true
case msg.Report != nil && addr == msg.Report.Serial:
@@ -125,7 +128,7 @@ func (l *Listener) send(msg UDPMsg) {
select {
case client <- msg:
default:
l.log.TRACE.Println("listener: recv blocked")
l.log.TRACE.Println("recv: listener blocked")
}
break
}
37 changes: 37 additions & 0 deletions charger/keba/sender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package keba

import (
"io"
"net"
"strings"

"github.com/andig/evcc/util"
)

// Sender is a KEBA UDP sender
type Sender struct {
conn *net.UDPConn
}

// NewSender creates KEBA UDP sender
func NewSender(addr string) (*Sender, error) {
addr = util.DefaultPort(addr, Port)
raddr, err := net.ResolveUDPAddr("udp", addr)

var conn *net.UDPConn
if err == nil {
conn, err = net.DialUDP("udp", nil, raddr)
}

c := &Sender{
conn: conn,
}

return c, err
}

// Send msg to receiver
func (c *Sender) Send(msg string) error {
_, err := io.Copy(c.conn, strings.NewReader(msg))
return err
}
142 changes: 142 additions & 0 deletions cmd/detect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package cmd

import (
"fmt"
"net"
"os"
"strings"

"github.com/andig/evcc/detect"
"github.com/andig/evcc/util"
"github.com/korylprince/ipnetgen"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// detectCmd represents the vehicle command
var detectCmd = &cobra.Command{
Use: "detect [host ...] [subnet ...]",
Short: "Auto-detect compatible hardware",
Long: `Automatic discovery using detect scans the local network for available devices.
Scanning focuses on devices that are commonly used that are detectable with reasonable efforts.
On successful detection, suggestions for EVCC configuration can be made. The suggestions should simplify
configuring EVCC but are probably not sufficient for fully automatic configuration.`,
Run: runDetect,
}

func init() {
rootCmd.AddCommand(detectCmd)
}

// IPsFromSubnet creates a list of ip addresses for given subnet
func IPsFromSubnet(arg string) (res []string) {
gen, err := ipnetgen.New(arg)
if err != nil {
log.FATAL.Fatal("could not create iterator")
}

for ip := gen.Next(); ip != nil; ip = gen.Next() {
res = append(res, ip.String())
}

return res
}

// ParseHostIPNet converts host or cidr into a host list
func ParseHostIPNet(arg string) (res []string) {
if ip := net.ParseIP(arg); ip != nil {
return []string{ip.String()}
}

_, ipnet, err := net.ParseCIDR(arg)

// simple host
if err != nil {
return []string{arg}
}

// check subnet size
if bits, _ := ipnet.Mask.Size(); bits < 24 {
log.INFO.Println("skipping large subnet:", ipnet)
return
}

return IPsFromSubnet(arg)
}

func display(res []detect.Result) {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"IP", "Hostname", "Task", "Details"})
table.SetAutoMergeCells(true)
table.SetRowLine(true)

for _, hit := range res {
switch hit.ID {
case detect.TaskPing, detect.TaskTCP80, detect.TaskTCP502:
continue

default:
host := ""
hosts, err := net.LookupAddr(hit.Host)
if err == nil && len(hosts) > 0 {
host = strings.TrimSuffix(hosts[0], ".")
}

details := ""
if hit.Details != nil {
details = fmt.Sprintf("%+v", hit.Details)
}

// fmt.Printf("%-16s %-20s %-16s %s\n", hit.Host, host, hit.ID, details)
table.Append([]string{hit.Host, host, hit.ID, details})
}
}

fmt.Println("")
table.Render()

fmt.Println(`
Please open https://github.com/andig/evcc/issues/new in your browser and copy the
results above into a new issue. Please tell us:
1. Is the scan result correct?
2. If not correct: please describe your hardware setup.`)
}

func runDetect(cmd *cobra.Command, args []string) {
util.LogLevel(viper.GetString("log"), nil)

println(viper.GetString("log"))
fmt.Println(`
Auto detection will now start to scan the network for available devices.
Scanning focuses on devices that are commonly used that are detectable with reasonable efforts.
On successful detection, suggestions for EVCC configuration can be made. The suggestions should simplify
configuring EVCC but are probably not sufficient for fully automatic configuration.`)
fmt.Println()

// args
var hosts []string
for _, arg := range args {
hosts = append(hosts, ParseHostIPNet(arg)...)
}

// autodetect
if len(hosts) == 0 {
ips := util.LocalIPs()
if len(ips) == 0 {
log.FATAL.Fatal("could not find ip")
}

myIP := ips[0]
log.INFO.Println("my ip:", myIP.IP)

hosts = append(hosts, "127.0.0.1")
hosts = append(hosts, IPsFromSubnet(myIP.String())...)
}

// magic happens here
res := detect.Work(log, 50, hosts)
display(res)
}
Loading

0 comments on commit 068fba5

Please sign in to comment.