evcc-io/cmd/configure/devicetest.go
2025-10-31 15:25:53 +00:00

159 lines
3.9 KiB
Go

package configure
import (
"context"
"errors"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger"
"github.com/evcc-io/evcc/meter"
"github.com/evcc-io/evcc/util/templates"
"github.com/evcc-io/evcc/vehicle"
"go.yaml.in/yaml/v4"
)
type DeviceTestResult string
const (
DeviceTestResultValid DeviceTestResult = "Valid"
DeviceTestResultValidMissingMeter DeviceTestResult = "Valid_MissingMeter"
DeviceTestResultInvalid DeviceTestResult = "Invalid"
)
type DeviceTest struct {
DeviceCategory DeviceCategory
Template templates.Template
ConfigValues map[string]any
}
// Test returns:
// - DeviceTestResult: Valid, Valid_MissingMeter, Invalid
// - error: != nil if the device is invalid and can not be configured with the provided settings
func (d *DeviceTest) Test() (DeviceTestResult, error) {
v, err := d.configure()
if err != nil {
return DeviceTestResultInvalid, err
}
switch DeviceCategories[d.DeviceCategory].class {
case templates.Charger:
return d.testCharger(v)
case templates.Meter:
return d.testMeter(d.DeviceCategory, v)
case templates.Vehicle:
return d.testVehicle(v)
default:
panic("invalid class for category: " + d.DeviceCategory)
}
}
// configure creates a configured device from a template so we can test it
func (d *DeviceTest) configure() (any, error) {
b, _, err := d.Template.RenderResult(templates.RenderModeInstance, d.ConfigValues)
if err != nil {
return nil, err
}
var instance struct {
Type string
Other map[string]any `yaml:",inline"`
}
if err := yaml.Unmarshal(b, &instance); err != nil {
return nil, err
}
var v any
ctx := context.TODO()
switch DeviceCategories[d.DeviceCategory].class {
case templates.Meter:
v, err = meter.NewFromConfig(ctx, instance.Type, instance.Other)
case templates.Charger:
v, err = charger.NewFromConfig(ctx, instance.Type, instance.Other)
case templates.Vehicle:
v, err = vehicle.NewFromConfig(ctx, instance.Type, instance.Other)
}
return v, err
}
// testCharger tests a charger device
func (d *DeviceTest) testCharger(v any) (DeviceTestResult, error) {
c, ok := v.(api.Charger)
if !ok {
return DeviceTestResultInvalid, errors.New("selected device is not a wallbox")
}
if _, err := c.Status(); err != nil {
return DeviceTestResultInvalid, err
}
m, ok := v.(api.Meter)
if !ok {
return DeviceTestResultValidMissingMeter, nil
}
if _, err := m.CurrentPower(); err != nil {
return DeviceTestResultInvalid, err
}
return DeviceTestResultValid, nil
}
// testMeter tests a meter device
func (d *DeviceTest) testMeter(deviceCategory DeviceCategory, v any) (DeviceTestResult, error) {
m, ok := v.(api.Meter)
if !ok {
return DeviceTestResultInvalid, errors.New("selected device is not a meter")
}
power, err := m.CurrentPower()
if err != nil {
return DeviceTestResultInvalid, err
}
// check if the grid meter reports power 0, which should be impossible
// happens with Kostal Piko charger that do not have a grid meter attached
// but we can't determine this
if power == 0 && deviceCategory == DeviceCategoryGridMeter {
return DeviceTestResultInvalid, errors.New("grid meter reports power 0")
}
if deviceCategory == DeviceCategoryBatteryMeter {
b, ok := v.(api.Battery)
if !ok {
return DeviceTestResultInvalid, errors.New("selected device is not a battery meter")
}
_, err := b.Soc()
for err != nil && errors.Is(err, api.ErrMustRetry) {
time.Sleep(3 * time.Second)
_, err = b.Soc()
}
if err != nil {
return DeviceTestResultInvalid, err
}
}
return DeviceTestResultValid, nil
}
// testVehicle tests a vehicle device
func (d *DeviceTest) testVehicle(v any) (DeviceTestResult, error) {
vv, ok := v.(api.Vehicle)
if !ok {
return DeviceTestResultInvalid, errors.New("selected device is not a vehicle")
}
if _, err := vv.Soc(); err != nil {
return DeviceTestResultInvalid, err
}
return DeviceTestResultValid, nil
}