390 lines
10 KiB
Go
390 lines
10 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 (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/request"
|
|
"github.com/evcc-io/evcc/util/sponsor"
|
|
"github.com/evcc-io/evcc/util/transport"
|
|
)
|
|
|
|
// EVECUBE charger implementation
|
|
type EVECUBE struct {
|
|
*request.Helper
|
|
uri string
|
|
connector int
|
|
user, pass string
|
|
statusCache time.Duration
|
|
cache time.Duration
|
|
currentLimit int64
|
|
}
|
|
|
|
func init() {
|
|
registry.Add("evecube", NewEVECUBEFromConfig)
|
|
}
|
|
|
|
//go:generate go tool decorate -f decorateEVECUBE -b *EVECUBE -r api.Charger -t api.PhaseSwitcher,api.Identifier
|
|
|
|
// EVECUBEUnitConfig is the /api/admin/unitconfig response
|
|
type EVECUBEUnitConfig struct {
|
|
ForcePhaseCharging int `json:"ForcePhaseCharging"`
|
|
NumberOfConnectors int `json:"NumberOfConnectors"`
|
|
MaxCurrent1 int `json:"MaxCurrent_1"`
|
|
MaxCurrent2 int `json:"MaxCurrent_2"`
|
|
MaxCurrent3 int `json:"MaxCurrent_3"`
|
|
MaxCurrent4 int `json:"MaxCurrent_4"`
|
|
}
|
|
|
|
// EVECUBEStatusResponse is the /api/admin/status response
|
|
type EVECUBEStatusResponse struct {
|
|
Connectors []EVECUBEConnectorStatus `json:"connectors"`
|
|
}
|
|
|
|
// EVECUBEConnectorStatus represents a single connector in the admin status response
|
|
type EVECUBEConnectorStatus struct {
|
|
ID int `json:"id"`
|
|
Status string `json:"status"`
|
|
Voltage float64 `json:"voltage"`
|
|
Voltages []float64 `json:"voltages"`
|
|
Current float64 `json:"current"`
|
|
Currents []float64 `json:"currents"`
|
|
Energy float64 `json:"energy"`
|
|
EnergyTotal float64 `json:"energyTotal"`
|
|
CarConnected bool `json:"carConnected"`
|
|
LastStatusPacket EVECUBELastStatusPacket `json:"lastStatusPacket"`
|
|
}
|
|
|
|
// EVECUBELastStatusPacket contains detailed status information
|
|
type EVECUBELastStatusPacket struct {
|
|
CarStatus string `json:"carStatus"`
|
|
ChargingStatus string `json:"chargingStatus"`
|
|
Voltage float64 `json:"voltage"`
|
|
Voltages []float64 `json:"voltages"`
|
|
Current float64 `json:"current"`
|
|
ActualWh float64 `json:"actualWh"`
|
|
TotalWh float64 `json:"totalWh"`
|
|
}
|
|
|
|
// EVECUBEAutomationStatus is the /api/admin/automation/status response
|
|
type EVECUBEAutomationStatus struct {
|
|
Connectors map[string]any `json:"connectors"`
|
|
AuthTag struct {
|
|
Tag string `json:"tag"`
|
|
} `json:"authTag"`
|
|
}
|
|
|
|
// EVECUBEUnitConfigRequest is the request body for /api/admin/unitconfig POST
|
|
type EVECUBEUnitConfigRequest struct {
|
|
Values map[string]any `json:"values"`
|
|
}
|
|
|
|
// NewEVECUBEFromConfig creates a EVECUBE charger from generic config
|
|
func NewEVECUBEFromConfig(other map[string]any) (api.Charger, error) {
|
|
cc := struct {
|
|
URI string
|
|
User string
|
|
Password string
|
|
Connector int
|
|
Cache time.Duration
|
|
}{
|
|
Connector: 1,
|
|
Cache: time.Second,
|
|
}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wb, err := NewEVECUBE(cc.URI, cc.User, cc.Password, cc.Connector, cc.Cache)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get unit configuration to determine connector count
|
|
config, err := wb.getUnitConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get unit config: %w", err)
|
|
}
|
|
|
|
if cc.Connector < 1 || cc.Connector > 4 || config.NumberOfConnectors < cc.Connector {
|
|
return nil, fmt.Errorf("invalid connector: %d", cc.Connector)
|
|
}
|
|
|
|
// Phases1p3p and Identify APIs affect the entire charger, not individual connectors
|
|
// Only enable these APIs if the charger has a single connector
|
|
var phases1p3p func(int) error
|
|
var identify func() (string, error)
|
|
|
|
if config.NumberOfConnectors == 1 {
|
|
phases1p3p = wb.phases1p3p
|
|
identify = wb.identify
|
|
}
|
|
|
|
return decorateEVECUBE(wb, phases1p3p, identify), nil
|
|
}
|
|
|
|
// NewEVECUBE creates EVECUBE charger
|
|
func NewEVECUBE(uri, user, password string, connector int, cache time.Duration) (*EVECUBE, error) {
|
|
log := util.NewLogger("evecube")
|
|
|
|
wb := &EVECUBE{
|
|
Helper: request.NewHelper(log),
|
|
uri: strings.TrimRight(uri, "/"),
|
|
connector: connector,
|
|
user: user,
|
|
pass: password,
|
|
statusCache: cache,
|
|
cache: cache,
|
|
currentLimit: 6,
|
|
}
|
|
|
|
if user != "" {
|
|
wb.Client.Transport = transport.BasicAuth(user, password, wb.Client.Transport)
|
|
}
|
|
|
|
if !sponsor.IsAuthorized() {
|
|
return nil, api.ErrSponsorRequired
|
|
}
|
|
|
|
return wb, nil
|
|
}
|
|
|
|
func (wb *EVECUBE) getUnitConfig() (EVECUBEUnitConfig, error) {
|
|
var config EVECUBEUnitConfig
|
|
uri := fmt.Sprintf("%s/api/admin/unitconfig", wb.uri)
|
|
|
|
if err := wb.GetJSON(uri, &config); err != nil {
|
|
return EVECUBEUnitConfig{}, err
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func (wb *EVECUBE) getStatus() (EVECUBEConnectorStatus, error) {
|
|
var resp EVECUBEStatusResponse
|
|
uri := fmt.Sprintf("%s/api/admin/status", wb.uri)
|
|
|
|
if err := wb.GetJSON(uri, &resp); err != nil {
|
|
return EVECUBEConnectorStatus{}, err
|
|
}
|
|
|
|
for _, connector := range resp.Connectors {
|
|
if connector.ID == wb.connector {
|
|
return connector, nil
|
|
}
|
|
}
|
|
|
|
return EVECUBEConnectorStatus{}, fmt.Errorf("connector %d not found", wb.connector)
|
|
}
|
|
|
|
func (wb *EVECUBE) getAutomationStatus() (EVECUBEAutomationStatus, error) {
|
|
var resp EVECUBEAutomationStatus
|
|
uri := fmt.Sprintf("%s/api/admin/automation/status", wb.uri)
|
|
|
|
if err := wb.GetJSON(uri, &resp); err != nil {
|
|
return EVECUBEAutomationStatus{}, err
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// Status implements the api.Charger interface
|
|
func (wb *EVECUBE) Status() (api.ChargeStatus, error) {
|
|
status, err := wb.getStatus()
|
|
if err != nil {
|
|
return api.StatusNone, err
|
|
}
|
|
|
|
if !status.CarConnected {
|
|
return api.StatusA, nil
|
|
}
|
|
|
|
if status.Status == "Charging" {
|
|
return api.StatusC, nil
|
|
}
|
|
|
|
return api.StatusB, nil
|
|
}
|
|
|
|
// Enabled implements the api.Charger interface
|
|
func (wb *EVECUBE) Enabled() (bool, error) {
|
|
config, err := wb.getUnitConfig()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Get the MaxCurrent for our connector
|
|
var maxCurrent int
|
|
switch wb.connector {
|
|
case 1:
|
|
maxCurrent = config.MaxCurrent1
|
|
case 2:
|
|
maxCurrent = config.MaxCurrent2
|
|
case 3:
|
|
maxCurrent = config.MaxCurrent3
|
|
case 4:
|
|
maxCurrent = config.MaxCurrent4
|
|
}
|
|
|
|
return maxCurrent > 0, nil
|
|
}
|
|
|
|
// Enable implements the api.Charger interface
|
|
func (wb *EVECUBE) Enable(enable bool) error {
|
|
var current int64
|
|
if enable {
|
|
current = wb.currentLimit
|
|
}
|
|
|
|
return wb.setValue(fmt.Sprintf("MaxCurrent_%d", wb.connector), current)
|
|
}
|
|
|
|
// MaxCurrent implements the api.Charger interface
|
|
func (wb *EVECUBE) MaxCurrent(current int64) error {
|
|
if current < 6 {
|
|
return fmt.Errorf("current must be >= 6A")
|
|
}
|
|
|
|
wb.currentLimit = current
|
|
|
|
return wb.setValue(fmt.Sprintf("MaxCurrent_%d", wb.connector), current)
|
|
}
|
|
|
|
// setValue sets a named value via admin API
|
|
func (wb *EVECUBE) setValue(key string, value int64) error {
|
|
reqBody := EVECUBEUnitConfigRequest{
|
|
Values: map[string]any{
|
|
key: value,
|
|
},
|
|
}
|
|
|
|
data, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uri := fmt.Sprintf("%s/api/admin/unitconfig", wb.uri)
|
|
req, err := request.New(http.MethodPost, uri, strings.NewReader(string(data)), request.JSONEncoding)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if wb.user != "" {
|
|
req.SetBasicAuth(wb.user, wb.pass)
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := wb.DoJSON(req, &resp); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var _ api.Meter = (*EVECUBE)(nil)
|
|
|
|
// CurrentPower implements the api.Meter interface
|
|
func (wb *EVECUBE) CurrentPower() (float64, error) {
|
|
status, err := wb.getStatus()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return status.Voltage * status.Current, nil
|
|
}
|
|
|
|
var _ api.MeterEnergy = (*EVECUBE)(nil)
|
|
|
|
// TotalEnergy implements the api.MeterEnergy interface
|
|
func (wb *EVECUBE) TotalEnergy() (float64, error) {
|
|
status, err := wb.getStatus()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return status.EnergyTotal, nil
|
|
}
|
|
|
|
var _ api.ChargeRater = (*EVECUBE)(nil)
|
|
|
|
// ChargedEnergy implements the api.ChargeRater interface
|
|
func (wb *EVECUBE) ChargedEnergy() (float64, error) {
|
|
status, err := wb.getStatus()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return float64(status.Energy) / 1000.0, nil
|
|
}
|
|
|
|
var _ api.PhaseCurrents = (*EVECUBE)(nil)
|
|
|
|
// Currents implements the api.PhaseCurrents interface
|
|
func (wb *EVECUBE) Currents() (float64, float64, float64, error) {
|
|
status, err := wb.getStatus()
|
|
if err != nil {
|
|
return 0, 0, 0, err
|
|
}
|
|
|
|
if len(status.Currents) == 3 {
|
|
return status.Currents[0], status.Currents[1], status.Currents[2], nil
|
|
}
|
|
|
|
return 0, 0, 0, nil
|
|
}
|
|
|
|
var _ api.PhaseVoltages = (*EVECUBE)(nil)
|
|
|
|
// Voltages implements the api.PhaseVoltages interface
|
|
func (wb *EVECUBE) Voltages() (float64, float64, float64, error) {
|
|
status, err := wb.getStatus()
|
|
if err != nil {
|
|
return 0, 0, 0, err
|
|
}
|
|
|
|
if len(status.LastStatusPacket.Voltages) == 3 {
|
|
return status.LastStatusPacket.Voltages[0], status.LastStatusPacket.Voltages[1], status.LastStatusPacket.Voltages[2], nil
|
|
}
|
|
|
|
return 0, 0, 0, nil
|
|
}
|
|
|
|
// phases1p3p implements the api.PhaseSwitcher interface
|
|
func (wb *EVECUBE) phases1p3p(phases int) error {
|
|
return whenDisabled(wb, func() error {
|
|
return wb.setValue("ForcePhaseCharging", int64(phases))
|
|
})
|
|
}
|
|
|
|
// identify implements the api.Identifier interface
|
|
func (wb *EVECUBE) identify() (string, error) {
|
|
status, err := wb.getAutomationStatus()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return status.AuthTag.Tag, nil
|
|
}
|