373 lines
9.2 KiB
Go
373 lines
9.2 KiB
Go
package charger
|
|
|
|
// LICENSE
|
|
|
|
// Copyright (c) evcc.io (andig, naltatis, premultiply)
|
|
|
|
// This module is NOT covered by the MIT license. All rights reserved.
|
|
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/modbus"
|
|
"github.com/evcc-io/evcc/util/sponsor"
|
|
"github.com/volkszaehler/mbmd/encoding"
|
|
)
|
|
|
|
// Ambibox is the Ambibox ambiCHARGE Home charger implementation.
|
|
type Ambibox struct {
|
|
log *util.Logger
|
|
conn *modbus.Connection
|
|
mu sync.Mutex
|
|
current float64
|
|
inputBase uint16
|
|
holdingBase uint16
|
|
input func() ([]byte, error) // cached bulk read of the input register block
|
|
}
|
|
|
|
// input register offsets (relative to inputBase), 2 registers per value
|
|
const (
|
|
ambiCurrentL1 = 4
|
|
ambiCurrentL2 = 6
|
|
ambiCurrentL3 = 8
|
|
ambiVoltageL1 = 12
|
|
ambiVoltageL2 = 14
|
|
ambiVoltageL3 = 16
|
|
ambiPowerAc = 18 // int32
|
|
ambiEnergyImport = 26 // float
|
|
ambiEnergyExport = 28 // float
|
|
ambiNumberPhases = 36 // uint32
|
|
ambiCapacity = 48 // float
|
|
ambiSoc = 54 // float
|
|
ambiSessionState = 82 // uint32 (EvseState)
|
|
ambiEvConnected = 84 // bool
|
|
ambiEnergyImportSess = 98 // float
|
|
ambiReplugRequired = 102 // bool
|
|
ambiInputLength = 104 // registers 0..103
|
|
)
|
|
|
|
// holding register offsets (relative to holdingBase), 2 registers per value
|
|
const (
|
|
ambiTargetPower = 0 // int32 W (negative = charge)
|
|
ambiWakeUp = 2 // uint32 (1 = wake up)
|
|
)
|
|
|
|
// EvseState enum values (session state)
|
|
const (
|
|
ambiStateSessionSetup = iota
|
|
ambiStateAuthorization
|
|
ambiStateChargeParameterDiscovery
|
|
ambiStateCableCheck
|
|
ambiStatePreCharge
|
|
ambiStateChargeLoop
|
|
ambiStatePostCharge
|
|
ambiStatePaused
|
|
ambiStateStopped
|
|
ambiStateError
|
|
)
|
|
|
|
func init() {
|
|
registry.AddCtx("ambibox", NewAmbiboxFromConfig)
|
|
}
|
|
|
|
// NewAmbiboxFromConfig creates an Ambibox charger from configuration
|
|
func NewAmbiboxFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
|
|
cc := struct {
|
|
modbus.TcpSettings `mapstructure:",squash"`
|
|
Connector int
|
|
}{
|
|
TcpSettings: modbus.TcpSettings{ID: 1},
|
|
Connector: 1,
|
|
}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cc.Connector < 1 || cc.Connector > 10 {
|
|
return nil, fmt.Errorf("invalid connector: %d", cc.Connector)
|
|
}
|
|
|
|
if !sponsor.IsAuthorized() {
|
|
return nil, api.ErrSponsorRequired
|
|
}
|
|
|
|
return NewAmbibox(ctx, cc.TcpSettings, cc.Connector)
|
|
}
|
|
|
|
// NewAmbibox creates an Ambibox charger
|
|
func NewAmbibox(ctx context.Context, settings modbus.TcpSettings, connector int) (*Ambibox, error) {
|
|
conn, err := settings.Connection(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log := util.NewLogger("ambibox")
|
|
conn.Logger(log.TRACE)
|
|
|
|
wb := &Ambibox{
|
|
log: log,
|
|
conn: conn,
|
|
inputBase: uint16(4000 + (connector-1)*200),
|
|
holdingBase: uint16(3000 + (connector-1)*100),
|
|
}
|
|
|
|
// share a single bulk read of the input block across all decoders
|
|
wb.input = util.Cached(func() ([]byte, error) {
|
|
return wb.conn.ReadInputRegisters(wb.inputBase, ambiInputLength)
|
|
}, time.Second)
|
|
|
|
return wb, nil
|
|
}
|
|
|
|
// decode helpers - byte offset in the input block is registerOffset*2
|
|
func f32(b []byte, off int) float64 { return float64(encoding.Float32(b[off*2:])) }
|
|
func i32(b []byte, off int) int32 { return encoding.Int32(b[off*2:]) }
|
|
func u32(b []byte, off int) uint32 { return encoding.Uint32(b[off*2:]) }
|
|
|
|
// writeRegister writes a 32-bit value to a holding register (2 registers)
|
|
func (wb *Ambibox) writeRegister(offset uint16, value uint32) error {
|
|
b := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(b, value)
|
|
_, err := wb.conn.WriteMultipleRegisters(wb.holdingBase+offset, 2, b)
|
|
|
|
return err
|
|
}
|
|
|
|
// targetWatts converts a charging current (A) into the Ambibox targetPower
|
|
// setpoint (DCW), based on measured voltage and active phases (VA).
|
|
func (wb *Ambibox) targetWatts(current float64) float64 {
|
|
phases := 3
|
|
var v1, v2, v3 float64 = 230, 230, 230
|
|
|
|
if b, err := wb.input(); err == nil {
|
|
if p := u32(b, ambiNumberPhases); p >= 1 && p <= 3 {
|
|
phases = int(p)
|
|
}
|
|
if v := f32(b, ambiVoltageL1); v > 0 {
|
|
v1 = v
|
|
}
|
|
if v := f32(b, ambiVoltageL2); v > 0 {
|
|
v2 = v
|
|
}
|
|
if v := f32(b, ambiVoltageL3); v > 0 {
|
|
v3 = v
|
|
}
|
|
}
|
|
|
|
sum := []float64{v1, v2, v3}
|
|
var v float64
|
|
for i := 0; i < phases; i++ {
|
|
v += sum[i]
|
|
}
|
|
|
|
// negative = charge
|
|
return -current * v
|
|
}
|
|
|
|
// setTargetPhaseCurrent writes the targetPower setpoint for the given current
|
|
func (wb *Ambibox) setTargetPhaseCurrent(current float64) error {
|
|
power := int32(math.Round(wb.targetWatts(current)))
|
|
return wb.writeRegister(ambiTargetPower, uint32(power))
|
|
}
|
|
|
|
// Status implements the api.Charger interface
|
|
func (wb *Ambibox) Status() (api.ChargeStatus, error) {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return api.StatusNone, err
|
|
}
|
|
|
|
if u32(b, ambiEvConnected) == 0 {
|
|
return api.StatusA, nil
|
|
}
|
|
|
|
if u32(b, ambiSessionState) == ambiStateChargeLoop {
|
|
return api.StatusC, nil
|
|
}
|
|
|
|
return api.StatusB, nil
|
|
}
|
|
|
|
var _ api.StatusReasoner = (*Ambibox)(nil)
|
|
|
|
// StatusReason implements the api.StatusReasoner interface
|
|
func (wb *Ambibox) StatusReason() (api.Reason, error) {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return api.ReasonUnknown, err
|
|
}
|
|
|
|
if u32(b, ambiReplugRequired) != 0 {
|
|
return api.ReasonDisconnectRequired, nil
|
|
}
|
|
if u32(b, ambiSessionState) == ambiStateAuthorization {
|
|
return api.ReasonWaitingForAuthorization, nil
|
|
}
|
|
|
|
return api.ReasonUnknown, nil
|
|
}
|
|
|
|
// Enabled implements the api.Charger interface
|
|
func (wb *Ambibox) Enabled() (bool, error) {
|
|
b, err := wb.conn.ReadHoldingRegisters(wb.holdingBase+ambiTargetPower, 2)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return encoding.Int32(b) != 0, nil
|
|
}
|
|
|
|
// Enable implements the api.Charger interface
|
|
func (wb *Ambibox) Enable(enable bool) error {
|
|
wb.mu.Lock()
|
|
defer wb.mu.Unlock()
|
|
|
|
var current float64
|
|
if enable {
|
|
current = wb.current
|
|
}
|
|
|
|
return wb.setTargetPhaseCurrent(current)
|
|
}
|
|
|
|
// MaxCurrent implements the api.Charger interface
|
|
func (wb *Ambibox) MaxCurrent(current int64) error {
|
|
return wb.MaxCurrentMillis(float64(current))
|
|
}
|
|
|
|
var _ api.ChargerEx = (*Ambibox)(nil)
|
|
|
|
// MaxCurrentMillis implements the api.ChargerEx interface
|
|
func (wb *Ambibox) MaxCurrentMillis(current float64) error {
|
|
wb.mu.Lock()
|
|
defer wb.mu.Unlock()
|
|
|
|
if err := wb.setTargetPhaseCurrent(current); err != nil {
|
|
return err
|
|
}
|
|
|
|
wb.current = current
|
|
return nil
|
|
}
|
|
|
|
var _ api.Meter = (*Ambibox)(nil)
|
|
|
|
// CurrentPower implements the api.Meter interface
|
|
func (wb *Ambibox) CurrentPower() (float64, error) {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return float64(i32(b, ambiPowerAc)), nil
|
|
}
|
|
|
|
var _ api.MeterEnergy = (*Ambibox)(nil)
|
|
|
|
// TotalEnergy implements the api.MeterEnergy interface
|
|
func (wb *Ambibox) TotalEnergy() (float64, error) {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return f32(b, ambiEnergyImport) / 1e3, nil
|
|
}
|
|
|
|
var _ api.MeterReturnEnergy = (*Ambibox)(nil)
|
|
|
|
// ReturnEnergy implements the api.MeterReturnEnergy interface
|
|
func (wb *Ambibox) ReturnEnergy() (float64, error) {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return f32(b, ambiEnergyExport) / 1e3, nil
|
|
}
|
|
|
|
var _ api.ChargeRater = (*Ambibox)(nil)
|
|
|
|
// ChargedEnergy implements the api.ChargeRater interface
|
|
func (wb *Ambibox) ChargedEnergy() (float64, error) {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return f32(b, ambiEnergyImportSess) / 1e3, nil
|
|
}
|
|
|
|
var _ api.PhaseCurrents = (*Ambibox)(nil)
|
|
|
|
// Currents implements the api.PhaseCurrents interface
|
|
func (wb *Ambibox) Currents() (float64, float64, float64, error) {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return 0, 0, 0, err
|
|
}
|
|
|
|
return f32(b, ambiCurrentL1), f32(b, ambiCurrentL2), f32(b, ambiCurrentL3), nil
|
|
}
|
|
|
|
var _ api.PhaseVoltages = (*Ambibox)(nil)
|
|
|
|
// Voltages implements the api.PhaseVoltages interface
|
|
func (wb *Ambibox) Voltages() (float64, float64, float64, error) {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return 0, 0, 0, err
|
|
}
|
|
|
|
return f32(b, ambiVoltageL1), f32(b, ambiVoltageL2), f32(b, ambiVoltageL3), nil
|
|
}
|
|
|
|
var _ api.Battery = (*Ambibox)(nil)
|
|
|
|
// Soc implements the api.Battery interface
|
|
func (wb *Ambibox) Soc() (float64, error) {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return f32(b, ambiSoc), nil
|
|
}
|
|
|
|
var _ api.BatteryCapacity = (*Ambibox)(nil)
|
|
|
|
// Capacity implements the api.BatteryCapacity interface
|
|
func (wb *Ambibox) Capacity() float64 {
|
|
b, err := wb.input()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
return f32(b, ambiCapacity) / 1e3
|
|
}
|
|
|
|
var _ api.Resurrector = (*Ambibox)(nil)
|
|
|
|
// WakeUp implements the api.Resurrector interface
|
|
func (wb *Ambibox) WakeUp() error {
|
|
return wb.writeRegister(ambiWakeUp, 1)
|
|
}
|