evcc-io/charger/voltie.go
2026-01-20 15:42:38 +01:00

252 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
"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"
)
// Voltie charger implementation
// https://voltie.eu
// Modbus API documentation v1.02
const (
voltieRegChargerID = 0x0000 // R, INT16, Voltie Charger ID
voltieRegFirmware = 0x0001 // R, INT16, FW version
voltieRegStatus = 0x000A // R, INT16, EVSE_STATE
voltieRegAutoStart = 0x000B // R/W, INT16, Auto Start enabled
voltieRegChargingEnabled = 0x000C // R/W, INT16, Charging enabled
voltieRegCharging = 0x000D // R, INT16, Charging (0=no charging, 1=charging)
voltieRegPhases = 0x000E // R, INT16, Number of phases in use
voltieRegStopReason = 0x0012 // R, INT16, Charge stop reason
voltieRegCurrentLimit = 0x0014 // R/W, INT16, Software current limit [mA]
voltieRegVoltages = 0x2000 // R, INT32, Phase L1 voltage [mV]
voltieRegCurrents = 0x2006 // R, INT32, Phase L1 charging current [mA]
voltieRegChargeDuration = 0x200C // R, INT32, Charge duration [s]
voltieRegChargedEnergy = 0x200E // R, INT32, Charged energy in current session [Ws]
voltieRegChargingPower = 0x2010 // R, INT32, Charging power [W]
)
// Voltie is an api.Charger implementation for Voltie wallboxes
type Voltie struct {
conn *modbus.Connection
}
func init() {
registry.AddCtx("voltie", NewVoltieFromConfig)
}
// NewVoltieFromConfig creates a Voltie charger from generic config
func NewVoltieFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
cc := struct {
modbus.TcpSettings `mapstructure:",squash"`
}{
TcpSettings: modbus.TcpSettings{
ID: 1,
},
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return NewVoltie(ctx, cc.URI, cc.ID)
}
// NewVoltie creates a Voltie charger
func NewVoltie(ctx context.Context, uri string, slaveID uint8) (*Voltie, error) {
conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
if err != nil {
return nil, err
}
if !sponsor.IsAuthorized() {
return nil, api.ErrSponsorRequired
}
log := util.NewLogger("voltie")
conn.Logger(log.TRACE)
wb := &Voltie{
conn: conn,
}
// Disable auto start
if _, err := wb.conn.WriteSingleRegister(voltieRegAutoStart, 0); err != nil {
return nil, err
}
return wb, nil
}
// Status implements the api.Charger interface
func (wb *Voltie) Status() (api.ChargeStatus, error) {
b, err := wb.conn.ReadHoldingRegisters(voltieRegStatus, 1)
if err != nil {
return api.StatusNone, err
}
status := binary.BigEndian.Uint16(b)
// EVSE states:
// 0x01: vehicle in state A – not connected
// 0x02: vehicle in state B – connected, ready
// 0x03: vehicle in state C – charging
// 0x04: vehicle in state D – charging, ventilation required
// 0x0D: vehicle in state E – vehicle error
// 0x05-0x0C, 0x0E-0x11: internal error states
// 0xFF: charger disabled, not functioning
switch status {
case 0x01:
return api.StatusA, nil
case 0x02:
return api.StatusB, nil
case 0x03, 0x04:
return api.StatusC, nil
default:
return api.StatusNone, fmt.Errorf("invalid status: %0x", status)
}
}
// Enabled implements the api.Charger interface
func (wb *Voltie) Enabled() (bool, error) {
b, err := wb.conn.ReadHoldingRegisters(voltieRegChargingEnabled, 1)
if err != nil {
return false, err
}
return binary.BigEndian.Uint16(b) != 0, nil
}
// Enable implements the api.Charger interface
func (wb *Voltie) Enable(enable bool) error {
var u uint16
if enable {
u = 1
}
_, err := wb.conn.WriteSingleRegister(voltieRegChargingEnabled, u)
return err
}
// MaxCurrent implements the api.Charger interface
func (wb *Voltie) MaxCurrent(current int64) error {
return wb.MaxCurrentMillis(float64(current))
}
var _ api.ChargerEx = (*Voltie)(nil)
// MaxCurrentMillis implements the api.ChargerEx interface
func (wb *Voltie) MaxCurrentMillis(current float64) error {
if current < 6 {
return fmt.Errorf("invalid current %.1f", current)
}
_, err := wb.conn.WriteSingleRegister(voltieRegCurrentLimit, uint16(current*1000))
return err
}
var _ api.Meter = (*Voltie)(nil)
// CurrentPower implements the api.Meter interface
func (wb *Voltie) CurrentPower() (float64, error) {
b, err := wb.conn.ReadHoldingRegisters(voltieRegChargingPower, 2)
if err != nil {
return 0, err
}
return float64(binary.BigEndian.Uint32(b)), nil
}
var _ api.ChargeRater = (*Voltie)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (wb *Voltie) ChargedEnergy() (float64, error) {
b, err := wb.conn.ReadHoldingRegisters(voltieRegChargedEnergy, 2)
if err != nil {
return 0, err
}
return float64(binary.BigEndian.Uint32(b)) / 3.6e6, nil // Ws to kWh
}
var _ api.PhaseCurrents = (*Voltie)(nil)
// Currents implements the api.PhaseCurrents interface
func (wb *Voltie) Currents() (float64, float64, float64, error) {
b, err := wb.conn.ReadHoldingRegisters(voltieRegCurrents, 6)
if err != nil {
return 0, 0, 0, err
}
var res [3]float64
for i := range res {
res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / 1e3 // mA to A
}
return res[0], res[1], res[2], nil
}
var _ api.PhaseVoltages = (*Voltie)(nil)
// Voltages implements the api.PhaseVoltages interface
func (wb *Voltie) Voltages() (float64, float64, float64, error) {
b, err := wb.conn.ReadHoldingRegisters(voltieRegVoltages, 6)
if err != nil {
return 0, 0, 0, err
}
var res [3]float64
for i := range res {
res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / 1e3 // mV to V
}
return res[0], res[1], res[2], nil
}
var _ api.Diagnosis = (*Voltie)(nil)
// Diagnose implements the api.Diagnosis interface
func (wb *Voltie) Diagnose() {
if b, err := wb.conn.ReadHoldingRegisters(voltieRegChargerID, 1); err == nil {
fmt.Printf("\tCharger ID:\t%d\n", binary.BigEndian.Uint16(b))
}
if b, err := wb.conn.ReadHoldingRegisters(voltieRegFirmware, 1); err == nil {
fmt.Printf("\tFirmware:\t%d\n", binary.BigEndian.Uint16(b))
}
if b, err := wb.conn.ReadHoldingRegisters(voltieRegStatus, 1); err == nil {
fmt.Printf("\tStatus:\t\t0x%04X\n", binary.BigEndian.Uint16(b))
}
if b, err := wb.conn.ReadHoldingRegisters(voltieRegPhases, 1); err == nil {
fmt.Printf("\tPhases:\t\t%d\n", binary.BigEndian.Uint16(b))
}
if b, err := wb.conn.ReadHoldingRegisters(voltieRegStopReason, 1); err == nil {
fmt.Printf("\tStop reason:\t%d\n", binary.BigEndian.Uint16(b))
}
}