326 lines
7.6 KiB
Go
326 lines
7.6 KiB
Go
package charger
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/charger/nrg/ble"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/godbus/dbus/v5"
|
|
"github.com/lunixbochs/struc"
|
|
"github.com/muka/go-bluetooth/bluez/profile/adapter"
|
|
"github.com/muka/go-bluetooth/bluez/profile/agent"
|
|
"github.com/muka/go-bluetooth/bluez/profile/device"
|
|
"github.com/muka/go-bluetooth/hw"
|
|
)
|
|
|
|
const nrgTimeout = 10 * time.Second
|
|
|
|
// NRGKickBLE charger implementation
|
|
type NRGKickBLE struct {
|
|
mu sync.Mutex
|
|
log *util.Logger
|
|
timer *time.Ticker
|
|
adapter *adapter.Adapter1
|
|
agent *agent.SimpleAgent
|
|
dev *device.Device1
|
|
device string
|
|
mac string
|
|
pin int
|
|
pauseCharging bool
|
|
current int
|
|
}
|
|
|
|
func init() {
|
|
registry.Add("nrgkick-bluetooth", NewNRGKickBLEFromConfig)
|
|
}
|
|
|
|
// NewNRGKickBLEFromConfig creates a NRGKickBLE charger from generic config
|
|
func NewNRGKickBLEFromConfig(other map[string]interface{}) (api.Charger, error) {
|
|
cc := struct{ Device, Mac, PIN string }{
|
|
Device: "hci0",
|
|
}
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// decode PIN with leading zero
|
|
pin, err := strconv.Atoi(cc.PIN)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid pin: %s", cc.PIN)
|
|
}
|
|
|
|
return NewNRGKickBLE(cc.Device, cc.Mac, pin)
|
|
}
|
|
|
|
// NewNRGKickBLE creates NRGKickBLE charger
|
|
func NewNRGKickBLE(device, mac string, pin int) (*NRGKickBLE, error) {
|
|
logger := util.NewLogger("nrg-bt")
|
|
|
|
ainfo, err := hw.GetAdapter(device)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
adapt, err := adapter.NewAdapter1FromAdapterID(ainfo.AdapterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Connect DBus System bus
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// do not reuse agent0 from service
|
|
agent.NextAgentPath()
|
|
|
|
ag := agent.NewSimpleAgent()
|
|
if err := agent.ExposeAgent(conn, ag, agent.CapNoInputNoOutput, true); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wb := &NRGKickBLE{
|
|
log: logger,
|
|
timer: time.NewTicker(2 * time.Second),
|
|
device: ainfo.AdapterID,
|
|
mac: mac,
|
|
pin: pin,
|
|
adapter: adapt,
|
|
agent: ag,
|
|
}
|
|
|
|
return wb, nil
|
|
}
|
|
|
|
func (wb *NRGKickBLE) connect() (*device.Device1, error) {
|
|
dev, err := ble.FindDevice(wb.adapter, wb.mac, nrgTimeout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("find device: %s", err)
|
|
}
|
|
|
|
err = ble.Connect(dev, wb.agent, wb.device)
|
|
return dev, err
|
|
}
|
|
|
|
func (wb *NRGKickBLE) close() {
|
|
if wb.dev != nil {
|
|
wb.dev.Close()
|
|
wb.dev = nil
|
|
}
|
|
}
|
|
|
|
func (wb *NRGKickBLE) read(service string, res interface{}) error {
|
|
wb.mu.Lock()
|
|
defer wb.mu.Unlock()
|
|
|
|
<-wb.timer.C
|
|
|
|
if wb.dev == nil {
|
|
dev, err := wb.connect()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wb.dev = dev
|
|
}
|
|
|
|
char, err := wb.dev.GetCharByUUID(service)
|
|
if err != nil {
|
|
wb.close()
|
|
return err
|
|
}
|
|
|
|
b, err := char.ReadValue(map[string]interface{}{})
|
|
if err != nil {
|
|
wb.close()
|
|
return err
|
|
}
|
|
wb.log.TRACE.Printf("read %s %0x", service, b)
|
|
|
|
return struc.Unpack(bytes.NewReader(b), res)
|
|
}
|
|
|
|
func (wb *NRGKickBLE) write(service string, val interface{}) error {
|
|
wb.mu.Lock()
|
|
defer wb.mu.Unlock()
|
|
|
|
var out bytes.Buffer
|
|
if err := struc.Pack(&out, val); err != nil {
|
|
return err
|
|
}
|
|
wb.log.TRACE.Printf("write %s %0x", service, out.Bytes())
|
|
|
|
<-wb.timer.C
|
|
|
|
if wb.dev == nil {
|
|
dev, err := wb.connect()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wb.dev = dev
|
|
}
|
|
|
|
char, err := wb.dev.GetCharByUUID(service)
|
|
if err != nil {
|
|
wb.close()
|
|
return err
|
|
}
|
|
|
|
if err := char.WriteValue(out.Bytes(), map[string]interface{}{}); err != nil {
|
|
wb.close()
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (wb *NRGKickBLE) mergeSettings(info ble.Info) ble.Settings {
|
|
return ble.Settings{
|
|
PIN: wb.pin,
|
|
ChargingEnergyLimit: 19997, // magic const for "disable"
|
|
KWhPer100: info.KWhPer100,
|
|
AmountPerKWh: info.AmountPerKWh,
|
|
Efficiency: info.Efficiency,
|
|
BLETransmissionPower: info.BLETransmissionPower,
|
|
PauseCharging: wb.pauseCharging, // apply last value
|
|
Current: wb.current, // apply last value
|
|
}
|
|
}
|
|
|
|
// Status implements the api.Charger interface
|
|
func (wb *NRGKickBLE) Status() (api.ChargeStatus, error) {
|
|
var res ble.Power
|
|
if err := wb.read(ble.PowerService, &res); err != nil {
|
|
return api.StatusNone, err
|
|
}
|
|
|
|
wb.log.TRACE.Printf("read power: %+v", res)
|
|
|
|
switch res.CPSignal {
|
|
case 3:
|
|
return api.StatusB, nil
|
|
case 2:
|
|
return api.StatusC, nil
|
|
case 4:
|
|
return api.StatusA, nil
|
|
default:
|
|
return api.StatusNone, fmt.Errorf("invalid status: %d", res.CPSignal)
|
|
}
|
|
}
|
|
|
|
// Enabled implements the api.Charger interface
|
|
func (wb *NRGKickBLE) Enabled() (bool, error) {
|
|
var res ble.Info
|
|
if err := wb.read(ble.InfoService, &res); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
wb.log.TRACE.Printf("read info: %+v", res)
|
|
|
|
// workaround internal NRGkick state change after connecting
|
|
// https://github.com/evcc-io/evcc/pull/274
|
|
return !res.PauseCharging || res.ChargingActive, nil
|
|
}
|
|
|
|
// Enable implements the api.Charger interface
|
|
func (wb *NRGKickBLE) Enable(enable bool) error {
|
|
var res ble.Info
|
|
if err := wb.read(ble.InfoService, &res); err != nil {
|
|
return err
|
|
}
|
|
|
|
// workaround internal NRGkick state change after connecting
|
|
// https://github.com/evcc-io/evcc/pull/274
|
|
if !enable && res.PauseCharging {
|
|
wb.pauseCharging = false
|
|
settings := wb.mergeSettings(res)
|
|
|
|
wb.log.TRACE.Printf("write settings (workaround): %+v", settings)
|
|
if err := wb.write(ble.SettingsService, &settings); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
wb.pauseCharging = !enable // use cached value to work around API roundtrip delay
|
|
settings := wb.mergeSettings(res)
|
|
|
|
wb.log.TRACE.Printf("write settings: %+v", settings)
|
|
|
|
return wb.write(ble.SettingsService, &settings)
|
|
}
|
|
|
|
// MaxCurrent implements the api.Charger interface
|
|
func (wb *NRGKickBLE) MaxCurrent(current int64) error {
|
|
var res ble.Info
|
|
if err := wb.read(ble.InfoService, &res); err != nil {
|
|
return err
|
|
}
|
|
|
|
wb.current = int(current) // use cached value to work around API roundtrip delay
|
|
settings := wb.mergeSettings(res)
|
|
|
|
wb.log.TRACE.Printf("write settings: %+v", settings)
|
|
|
|
return wb.write(ble.SettingsService, &settings)
|
|
}
|
|
|
|
var _ api.Meter = (*NRGKickBLE)(nil)
|
|
|
|
// CurrentPower implements the api.Meter interface
|
|
func (wb *NRGKickBLE) CurrentPower() (float64, error) {
|
|
var res ble.Power
|
|
if err := wb.read(ble.PowerService, &res); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
wb.log.TRACE.Printf("read power: %+v", res)
|
|
|
|
return float64(res.TotalPower) * 10, nil
|
|
}
|
|
|
|
var _ api.MeterEnergy = (*NRGKickBLE)(nil)
|
|
|
|
// TotalEnergy implements the api.MeterEnergy interface
|
|
func (wb *NRGKickBLE) TotalEnergy() (float64, error) {
|
|
var res ble.Energy
|
|
if err := wb.read(ble.EnergyService, &res); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
wb.log.TRACE.Printf("read energy: %+v", res)
|
|
|
|
return float64(res.TotalEnergy) / 1000, nil
|
|
}
|
|
|
|
var _ api.PhaseCurrents = (*NRGKickBLE)(nil)
|
|
|
|
// Currents implements the api.PhaseCurrents interface
|
|
func (wb *NRGKickBLE) Currents() (float64, float64, float64, error) {
|
|
var res ble.VoltageCurrent
|
|
if err := wb.read(ble.VoltageCurrentService, &res); err != nil {
|
|
return 0, 0, 0, err
|
|
}
|
|
|
|
wb.log.TRACE.Printf("read voltage/current: %+v", res)
|
|
|
|
return float64(res.CurrentL1) / 100,
|
|
float64(res.CurrentL2) / 100,
|
|
float64(res.CurrentL3) / 100,
|
|
nil
|
|
}
|
|
|
|
// ChargedEnergy implements the ChargeRater interface
|
|
// NOTE: apparently shows energy of a stopped charging session, hence substituted by TotalEnergy
|
|
// func (wb *NRGKickBLE) ChargedEnergy() (float64, error) {
|
|
// res := ble.Energy{}
|
|
// if err := wb.read(ble.EnergyService, &res); err != nil {
|
|
// return 0, err
|
|
// }
|
|
// wb.log.TRACE.Printf("energy: %+v", res)
|
|
// return float64(res.EnergyLastCharge) / 1000, nil
|
|
// }
|