evcc-io/charger/trydan.go
2026-05-16 11:54:37 +02:00

222 lines
5.8 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.
// https://v2charge.com/trydan/
import (
"errors"
"fmt"
"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"
)
type RealTimeData struct {
ID string `json:"ID"`
ChargeState int `json:"ChargeState"`
ReadyState int `json:"ReadyState"`
ChargePower float64 `json:"ChargePower"`
ChargeEnergy float64 `json:"ChargeEnergy"`
SlaveError int `json:"SlaveError"`
ChargeTime int `json:"ChargeTime"`
HousePower float64 `json:"HousePower"`
FVPower float64 `json:"FVPower"`
BatteryPower float64 `json:"BatteryPower"`
Paused int `json:"Paused"`
Locked int `json:"Locked"`
Timer int `json:"Timer"`
Intensity int `json:"Intensity"`
Dynamic int `json:"Dynamic"`
MinIntensity int `json:"MinIntensity"`
MaxIntensity int `json:"MaxIntensity"`
PauseDynamic int `json:"PauseDynamic"`
FirmwareVersion string `json:"FirmwareVersion"`
DynamicPowerMode int `json:"DynamicPowerMode"`
ContractedPower int `json:"ContractedPower"`
}
// Trydan charger implementation
type Trydan struct {
*request.Helper
uri string
statusG util.Cacheable[RealTimeData]
current int
enabled bool
}
func init() {
registry.Add("trydan", NewTrydanFromConfig)
}
// NewTrydanFromConfig creates a Trydan charger from generic config
func NewTrydanFromConfig(other map[string]any) (api.Charger, error) {
cc := struct {
URI string
Cache time.Duration
}{
Cache: time.Second,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.URI == "" {
return nil, errors.New("missing uri")
}
return NewTrydan(cc.URI, cc.Cache)
}
// NewTrydan creates Trydan charger
func NewTrydan(uri string, cache time.Duration) (api.Charger, error) {
if !sponsor.IsAuthorized() {
return nil, api.ErrSponsorRequired
}
c := &Trydan{
Helper: request.NewHelper(util.NewLogger("trydan")),
uri: util.DefaultScheme(strings.TrimSuffix(uri, "/"), "http"),
}
c.statusG = util.ResettableCached(func() (RealTimeData, error) {
var res RealTimeData
uri := fmt.Sprintf("%s/RealTimeData", c.uri)
err := c.GetJSON(uri, &res)
return res, err
}, cache)
return c, nil
}
// Status implements the api.Charger interface
func (t Trydan) Status() (api.ChargeStatus, error) {
data, err := t.statusG.Get()
if err != nil {
return api.StatusNone, err
}
switch state := data.ChargeState; state {
case 0:
return api.StatusA, nil
case 1:
return api.StatusB, nil
case 2:
return api.StatusC, nil
default:
return api.StatusNone, fmt.Errorf("unknown status: %d", state)
}
}
// Enabled implements the api.Charger interface
func (c Trydan) Enabled() (bool, error) {
data, err := c.statusG.Get()
return data.Paused == 0 && data.Locked == 0, err
}
func (c *Trydan) setValue(param string, value int) error {
uri := fmt.Sprintf("%s/write/%s=%d", c.uri, param, value)
res, err := c.GetBody(uri)
if str := string(res); err == nil && str != "OK" {
err = fmt.Errorf("command failed: %s", res)
}
return err
}
// Enable implements the api.Charger interface
func (c Trydan) Enable(enable bool) error {
var pause, pauseDynamic int
if !enable {
pause = 1
} else {
pauseDynamic = 1
}
if err := c.setValue("Paused", pause); err != nil {
return err
}
if err := c.setValue("Locked", pause); err != nil {
return err
}
// Pause/Unpause Dynamic Power Control if enabled.
// This is needed to let EVCC taking over charging power control.
// Charger will stop returning power readings if 'Dynamic' is disabled.
data, err := c.statusG.Get()
if err != nil {
return err
}
if data.Dynamic == 1 {
if err := c.setValue("PauseDynamic", pauseDynamic); err != nil {
// Pause V2C 'PauseDynamic' when EVCC charging is active and vice versa.
return err
}
}
c.enabled = enable
return nil
}
// MaxCurrent implements the api.Charger interface
func (c Trydan) MaxCurrent(current int64) error {
err := c.setValue("Intensity", int(current))
if err == nil {
c.current = int(current)
}
return err
}
// removed broken interfaces https://github.com/evcc-io/evcc/issues/28047
// var _ api.ChargeRater = (*Trydan)(nil)
// // ChargedEnergy implements the api.ChargeRater interface
// func (c Trydan) ChargedEnergy() (float64, error) {
// data, err := c.statusG.Get()
// return data.ChargeEnergy, err
// }
// var _ api.ChargeTimer = (*Trydan)(nil)
// // ChargeDuration implements the api.ChargeTimer interface
// func (c Trydan) ChargeDuration() (time.Duration, error) {
// data, err := c.statusG.Get()
// return time.Duration(data.ChargeTime) * time.Second, err
// }
var _ api.Meter = (*Trydan)(nil)
// CurrentPower implements the api.Meter interface
func (c Trydan) CurrentPower() (float64, error) {
data, err := c.statusG.Get()
return data.ChargePower, err
}
var _ api.Diagnosis = (*Trydan)(nil)
// Diagnose implements the api.Diagnosis interface
func (c *Trydan) Diagnose() {
data, err := c.statusG.Get()
if err != nil {
fmt.Printf("%#v", data)
}
}