Skip to content

Commit

Permalink
Implement basic hardware detection (#435)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Nov 21, 2020
1 parent f093cc5 commit 068fba5
Show file tree
Hide file tree
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
Expand Up @@ -4,10 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"reflect"
"strings"
"time"

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

const (
udpTimeout = time.Second
kebaPort = 7090
)

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

func init() {
Expand All @@ -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{}) {
Expand Down Expand Up @@ -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
}

Expand Down
23 changes: 13 additions & 10 deletions charger/keba/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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() {
Expand All @@ -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
}

Expand Down Expand Up @@ -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:
Expand All @@ -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
}
Expand Down
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.