diff --git a/README.md b/README.md index c70dcfea1..171a91be7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Evcc is an extensible EV Charge Controller with PV integration implemented in [G - simple and clean user interface - multiple [chargers](https://docs.evcc.io/docs/devices/chargers): - Open source: [openWB](https://openwb.de/), [EVSEWifi](https://www.evse-wifi.de) (includes smartWB) - - Other commercial: ABL eMH1, Alfen Eve, cFos PowerBrain, Daheimladen, go-eCharger, Heidelberg Energy Control, Innogy eBox, KEBA/BMW, NRGkick, Wallbe, Mobile Charger Connect, EEBUS (experimental) + - Other commercial: ABL eMH1, Alfen Eve, cFos PowerBrain, Daheimladen, go-eCharger, Heidelberg Energy Control, Innogy eBox, KEBA/BMW, NRGkick, PC Electric (includes Garo), Wallbe, Mobile Charger Connect, EEBUS (experimental) - Build-your-own: Phoenix (includes ESL Walli), [SimpleEVSE](https://www.evse-wifi.de/produkt-schlagwort/simple-evse-wb/) - Smart-Home outlets: FritzDECT, Shelly, Tasmota, TP-Link - multiple [meters](https://docs.evcc.io/docs/devices/meters): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, ...), Tesla PowerWall, LG ESS HOME diff --git a/charger/pcelectric.go b/charger/pcelectric.go new file mode 100644 index 000000000..1e1ff8466 --- /dev/null +++ b/charger/pcelectric.go @@ -0,0 +1,279 @@ +package charger + +import ( + "fmt" + "net/http" + "strings" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/charger/pcelectric" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" +) + +// PCElectric charger implementation +type PCElectric struct { + *request.Helper + log *util.Logger + + uri string // http://garo2216247:8080/servlet + slaveIndex int // 0 = Master, 1..n Slave + meter string // + + lbmode bool // true/false (wird automatisch bestimmt) + serialNumber int // 1234567 +} + +func init() { + registry.Add("garo", NewPCElectricFromConfig) + registry.Add("pcelectric", NewPCElectricFromConfig) +} + +//go:generate go run ../cmd/tools/decorate.go -f decoratePCE -b *PCElectric -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)" + +// NewPCElectricFromConfig creates a PCElectric charger from generic config +func NewPCElectricFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := struct { + URI string + SlaveIndex int + Meter string + }{ + Meter: "CENTRAL100", + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + wb, err := NewPCElectric(util.DefaultScheme(cc.URI, "http"), cc.SlaveIndex, cc.Meter) + if err == nil && wb.slaveIndex == 0 { // Nur Master hat den Zähler...leider + var res pcelectric.MeterInfo + if err := wb.GetJSON(wb.meter, &res); err == nil && res.MeterSerial != "" { + return decoratePCE(wb, wb.currentPower, wb.totalEnergy, wb.currents), nil + } + + wb.meter = "" + } + + return wb, err +} + +// NewPCElectric creates PCElectric charger +func NewPCElectric(uri string, slaveIndex int, meter string) (*PCElectric, error) { + log := util.NewLogger("pce") + uri = strings.TrimSuffix(strings.TrimRight(uri, "/"), "/servlet") + "/servlet/rest/chargebox" + + wb := &PCElectric{ + Helper: request.NewHelper(log), + log: log, + uri: uri, + slaveIndex: slaveIndex, + meter: strings.ToUpper(meter), + } + + // Nur Master: lb Config auslesen. + // Ohne Loadbalancer: Steuerung über currentlimit + // Mit Loadbalander: Steuerung über loadBalancingFuse + var lbconfig pcelectric.LbConfig + uri = fmt.Sprintf("%s/lbconfig/false", wb.uri) + if err := wb.GetJSON(uri, &lbconfig); err == nil { + wb.lbmode = lbconfig.MasterLoadBalanced + wb.serialNumber = lbconfig.Slaves[wb.slaveIndex].SerialNumber + log.DEBUG.Printf("lbmode: %t serial: %d ", wb.lbmode, wb.serialNumber) + } + + return wb, nil +} + +// Status implements the api.Charger interface +func (wb *PCElectric) Status() (api.ChargeStatus, error) { + var chargeStatus int + var sessionStartTime int64 + + if wb.slaveIndex == 0 { + var status pcelectric.Status + + uri := fmt.Sprintf("%s/status", wb.uri) + if err := wb.GetJSON(uri, &status); err != nil { + return api.StatusNone, err + } + chargeStatus = status.ChargeStatus + sessionStartTime = status.SessionStartTime + } else { + var status pcelectric.SlaveStatus + + uri := fmt.Sprintf("%s/slaves/false", wb.uri) + if err := wb.GetJSON(uri, &status); err != nil { + return api.StatusNone, err + } + if wb.slaveIndex >= len(status) { + return api.StatusNone, nil + } + chargeStatus = status[wb.slaveIndex].ChargeStatus + sessionStartTime = status[wb.slaveIndex].SessionStartTime + } + wb.log.DEBUG.Printf("chargeStatus: %d", chargeStatus) + + var res api.ChargeStatus + switch chargeStatus { + case 0x00, 0x10: // notconnected + res = api.StatusA + case 0x30: // connected + res = api.StatusB + case 0x40: // charging + res = api.StatusC + case 0x42, // chargepaused + 0x50, // chargefinished + 0x60: // chargecancelled + res = api.StatusB + case 0x90: // unavailable + if sessionStartTime > 0 { + res = api.StatusB + } else { + res = api.StatusF + } + case 0x95, // dcfault + 0x96, // dchardwarefault + 0x9A, // cpfault + 0x9B: // cpshorted + res = api.StatusE + case 0x70, // overheat + 0x80, // criticaltemperature + 0x91, // reserved + 0x9C, // remotedisabled + 0x9D, // dlmfault + 0xA0, // cablefault + 0xA1, + 0xA2, // lockingfault + 0xA3, + 0xA4, // contactorfault + 0xA8, // rcdfault + 0xF0, // wait + 0xF1: // ventfault + res = api.StatusF + default: // generalfault + res = api.StatusF + } + + return res, nil +} + +// Enabled implements the api.Charger interface +func (wb *PCElectric) Enabled() (bool, error) { + var res pcelectric.Status + uri := fmt.Sprintf("%s/status", wb.uri) + err := wb.GetJSON(uri, &res) + if err == nil && res.PowerMode == "ON" { + return true, err + } + return false, err +} + +// Enable implements the api.Charger interface +func (wb *PCElectric) Enable(enable bool) error { + if wb.slaveIndex > 0 { + return nil //Slave wird immer mit dem Master geschaltet! + } + + // Master Only !! + mode := "ALWAYS_OFF" + if enable { + mode = "ALWAYS_ON" + } + + uri := fmt.Sprintf("%s/mode/%s", wb.uri, mode) + req, err := request.New(http.MethodPost, uri, nil, request.JSONEncoding) + if err == nil { + _, err = wb.DoBody(req) + } + + return err +} + +func (wb *PCElectric) MinCurrent(current int64) error { + data := pcelectric.MinCurrentLimitStruct{ + { + MinCurrentLimit: int(current), // default=6 + SerialNumber: wb.serialNumber, + TwinSerial: -1, + }, + } + + uri := fmt.Sprintf("%s/mincurrentlimit", wb.uri) + req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + if err == nil { + _, err = wb.DoBody(req) + } + + return err +} + +// MaxCurrent implements the api.Charger interface +func (wb *PCElectric) MaxCurrent(current int64) error { + if wb.slaveIndex > 0 { + return nil //Slave wird immer mit dem Master geschaltet! + } + + // Ohne Loadbalancer Regelung über currentlimit: + if !wb.lbmode { + data := pcelectric.ReducedIntervals{ + ReducedIntervalsEnabled: true, + ReducedCurrentIntervals: []pcelectric.ReducedCurrentInterval{ + { + SchemaId: 1, + Start: "00:00:00", + Stop: "24:00:00", + Weekday: 8, + ChargeLimit: int(current), + }, + }, + } + + uri := fmt.Sprintf("%s/currentlimit", wb.uri) + req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + if err == nil { + _, err = wb.DoBody(req) + } + + return err + } + + // Mit Loadbalancer Regelung über lbconfig/LoadBalancingFuse + var data pcelectric.LbConfigShort + uri := fmt.Sprintf("%s/lbconfig/false", wb.uri) + if err := wb.GetJSON(uri, &data); err != nil { + return err + } + + data.LoadBalancingFuse = int(current) + + uri = fmt.Sprintf("%s/lbconfig", wb.uri) + req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) + if err == nil { + _, err = wb.DoBody(req) + } + + return err +} + +// CurrentPower implements the api.Meter interface W +func (wb *PCElectric) currentPower() (float64, error) { + l1, l2, l3, err := wb.currents() + return 230 * (l1 + l2 + l3), err +} + +// TotalEnergy implements the api.MeterEnergy interface kwh +func (wb *PCElectric) totalEnergy() (float64, error) { + var res pcelectric.MeterInfo + uri := fmt.Sprintf("%s/meterinfo/%s", wb.uri, wb.meter) + err := wb.GetJSON(uri, &res) + return float64(res.AccEnergy) / 1e3, err +} + +// Currents implements the api.MeterCurrents interface A +func (wb *PCElectric) currents() (float64, float64, float64, error) { + var res pcelectric.MeterInfo + uri := fmt.Sprintf("%s/meterinfo/%s", wb.uri, wb.meter) + err := wb.GetJSON(uri, &res) + return float64(res.Phase1Current) / 10, float64(res.Phase2Current) / 10, float64(res.Phase3Current) / 10, err +} diff --git a/charger/pcelectric/types.go b/charger/pcelectric/types.go new file mode 100644 index 000000000..884a47bb2 --- /dev/null +++ b/charger/pcelectric/types.go @@ -0,0 +1,244 @@ +package pcelectric + +// /servlet/rest/chargebox/status + +type Status struct { + SerialNumber int64 // 2216247, + Connector string // "NOT_CONNECTED", + Mode string // "ALWAYS_ON", + CurrentLimit int // 11, + FactoryCurrentLimit int // 32, + SwitchCurrentLimit int // 32, + PowerMode string // "OFF", + CurrentChargingCurrent int // -1, + CurrentChargingPower int // -1, + AccSessionEnergy int // 0, + SessionStartTime int64 // 1641489661842 + ChargeboxTime string // "22:25" + AccSessionMillis int64 // 0, + LatestReading int // 0, + ChargeStatus int // 0, + NrOfPhases int // 1, + MainCharger struct { + Reference string // "Garage", + SerialNumber int // 2216247, + LastContact int64 // 1640781615305, + Online bool // false, + LoadBalanced bool // true, + Phase int // 16, + ProductId int // 121, + MeterStatus int // 1, + MeterSerial string // "", + ChargeStatus int // 0, + PilotLevel int // 6, + AccEnergy int // -1, + Connector string // "NOT_CONNECTED", + AccSessionEnergy int // 0, + SessionStartValue int64 // -1, + AccSessionMillis int64 // 0, + CurrentChargingCurrent int // -1, + CurrentChargingPower int // 0, + NrOfPhases int // 1, + TwinSerial int // -1, + } + TwinCharger interface{} +} + +// /servlet/rest/chargebox/slaves/false +type SlaveStatus []struct { + Reference string // "Garage", + SerialNumber int // 2216247, + LastContact int64 // 1640781615305, + Online bool // false, + LoadBalanced bool // true, + Phase int // 16, + ProductId int // 121, + MeterStatus int // 1, + MeterSerial string // "", + ChargeStatus int // 0, + PilotLevel int // 6, + AccEnergy int // -1, + FirmwareVersion int // 7 + FirmwareRevision int // 9 + WifiCardStatus int // 2 + Connector string // "NOT_CONNECTED", + AccSessionEnergy int // 0, + SessionStartValue int // -1, + AccSessionMillis int64 // 0, + SessionStartTime int64 // 1641136092090 + CurrentChargingCurrent int // -1, + CurrentChargingPower int // 0, + NrOfPhases int // 1, + TwinSerial int // -1, + CableLockMode int // 0 + MinCurrentLimit int // 6 + DipSwitchSettings int // 8188 +} + +type ReducedIntervals struct { + ReducedIntervalsEnabled bool `json:"reducedIntervalsEnabled"` + ReducedCurrentIntervals []ReducedCurrentInterval `json:"reducedCurrentIntervals,omitempty"` +} + +type ReducedCurrentInterval struct { + SchemaId int `json:"schemaId"` + Start string `json:"start"` + Stop string `json:"stop"` + Weekday int `json:"weekday"` + ChargeLimit int `json:"chargeLimit"` +} + +// /servlet/rest/chargebox/meterinfo/CENTRAL100 + +type MeterInfo struct { + Success int // 0, + AccEnergy int64 // 169100, + Phase1Current int // 158, + Phase2Current int // 157, + Phase3Current int // 156, + Phase1InstPower int // 3, + Phase2InstPower int // 3, + Phase3InstPower int // 3, + ReadTime int64 // 1640783279023, + GridNetType string // "UNKNOWN", + MeterSerial string // "116223V", + Type int // 341, + ApparentPower int // 9 +} + +// /servlet/rest/chargebox/lbconfig/false +type LbConfigShort struct { + LoadBalancingFuse int `json:"loadBalancingFuse"` // 32 + LoadBalancingPower int `json:"loadBalancingPower"` // 0 + LoadBalancingFuse101 int `json:"loadBalancingFuse101"` // 32 + LoadBalancingPower101 int `json:"loadBalancingPower101"` // 0 + MasterPhase int `json:"masterPhase"` // 16 + MasterLoadBalanced bool `json:"masterLoadBalanced"` // true +} + +type LbConfig struct { + LoadBalancingFuse int // 16 + LoadBalancingPower int // 0 + LoadBalancingFuse101 int // 16 + LoadBalancingPower101 int // 0 + MasterPhase int // 16 + MasterLoadBalanced bool // true + Slaves []struct { + Reference string // "Garage" + SerialNumber int // 2216247 + LastContact int64 // 1640970181816 + Online bool // false + LoadBalanced bool // true + Phase int // 16 + ProductId int // 121 + MeterStatus int // 1 + MeterSerial string // "" + ChargeStatus int // 144 + PilotLevel int // 6 + AccEnergy int // -1 + FirmwareVersion int // 7 + FirmwareRevision int // 9 + WifiCardStatus int // 2 + Connector string // "UNAVAILABLE" + AccSessionEnergy int // 0 + SessionStartValue int // -1 + AccSessionMillis int64 // 174723 + SessionStartTime int64 // 1640957660208 + CurrentChargingCurrent int // -1 + CurrentChargingPower int // 0 + NrOfPhases int // 1 + TwinSerial int // -1 + CableLockMode int // 0 + MinCurrentLimit int // 6 + DipSwitchSettings int // 8188 + } +} +type Config struct { + OcppConnected bool // false + MaxChargeCurrent int // 32 + ProductId string // "81" + ProgramVersion string // "7.9" + FirmwareVersion int // 7 + FirmwareRevision int // 9 + SmallFirmwareVersion int // 2 + SmallFirmwareRevision int // 15 + LargeFirmwareVersion int // 7 + LargeFirmwareRevision int // 9 + LbVersion2 bool // true + SerialNumber int // 2216247 + MeterSerialNumber string // "" + MeterType int // 0 + FactoryChargeLimit int // 32 + SwitchChargeLimit int // 32 + RfidReaderPresent bool // true + RfidMode string // "RFID_WIFI" + PowerbackupStatus int // 2 + LastTemperature int // 25 + WarningTemperature int // 65 + CutoffTemperature int // 70 + ReducedIntervalsEnabled bool // true + ReducedCurrentIntervals []struct { + SchemaId int // 1 + Start string // "00:00:00" + Stop string // "00:00:00" + Weekday int // 8 + ChargeLimit int // 6 + } + SoftwareVersion int // 185 + AvailableVersion int // 185 + UpdateUrl string // "http%3A%2F%2F85.11.39.104%2Fchargebox_185.tgz" + NetworkMode int // 1 + NetworkType int // 0 + NetworkSSID string // "SchlockeNet" + WebNetworkPassword string // "" + NetworkAPChannel int // 6 + EthNetworkMode int // 0 + GcConfigTimestamp int64 // null + GcloudActivated bool // false + GcActivatedFrom int64 // null + EthGateway string // "" + EthDNS string // "" + EthIP string // "" + EthMask int // 24 + LocalLoadBalanced bool // false + GroupLoadBalanced bool // true + GroupLoadBalanced101 bool // false + EnergyReportEnabled bool // false + Master bool // true + Timezone string // null + SlaveList []struct { + Reference string // Garage + SerialNumber int // 2216247 + LastContact int64 // 1640960502344 + Online bool // false + LoadBalanced bool // true + Phase int // 16 + ProductId int // 121 + MeterStatus int // 1 + MeterSerial string // "" + ChargeStatus int // 144 + PilotLevel int // 6 + AccEnergy int // -1 + FirmwareVersion int // 7 + FirmwareRevision int // 9 + WifiCardStatus int // 2 + Connector string // "UNAVAILABLE" + AccSessionEnergy int // 0 + SessionStartValue int // -1 + AccSessionMillis int // 5635 + SessionStartTime int64 // 1640957660208 + CurrentChargingCurrent int // -1 + CurrentChargingPower int // 0 + NrOfPhases int // 1 + TwinSerial int // -1 + CableLockMode int // 0 + MinCurrentLimit int // 6 + DipSwitchSettings int // 8188 + } +} + +type MinCurrentLimitStruct []struct { + MinCurrentLimit int `json:"minCurrentLimit"` + SerialNumber int `json:"serialNumber"` + TwinSerial int `json:"twinSerial"` // -1 +} diff --git a/charger/pcelectric_decorators.go b/charger/pcelectric_decorators.go new file mode 100644 index 000000000..0e4b6c539 --- /dev/null +++ b/charger/pcelectric_decorators.go @@ -0,0 +1,137 @@ +package charger + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decoratePCE(base *PCElectric, meter func() (float64, error), meterEnergy func() (float64, error), meterCurrent func() (float64, float64, float64, error)) api.Charger { + switch { + case meter == nil && meterCurrent == nil && meterEnergy == nil: + return base + + case meter != nil && meterCurrent == nil && meterEnergy == nil: + return &struct { + *PCElectric + api.Meter + }{ + PCElectric: base, + Meter: &decoratePCEMeterImpl{ + meter: meter, + }, + } + + case meter == nil && meterCurrent == nil && meterEnergy != nil: + return &struct { + *PCElectric + api.MeterEnergy + }{ + PCElectric: base, + MeterEnergy: &decoratePCEMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case meter != nil && meterCurrent == nil && meterEnergy != nil: + return &struct { + *PCElectric + api.Meter + api.MeterEnergy + }{ + PCElectric: base, + Meter: &decoratePCEMeterImpl{ + meter: meter, + }, + MeterEnergy: &decoratePCEMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case meter == nil && meterCurrent != nil && meterEnergy == nil: + return &struct { + *PCElectric + api.MeterCurrent + }{ + PCElectric: base, + MeterCurrent: &decoratePCEMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + } + + case meter != nil && meterCurrent != nil && meterEnergy == nil: + return &struct { + *PCElectric + api.Meter + api.MeterCurrent + }{ + PCElectric: base, + Meter: &decoratePCEMeterImpl{ + meter: meter, + }, + MeterCurrent: &decoratePCEMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + } + + case meter == nil && meterCurrent != nil && meterEnergy != nil: + return &struct { + *PCElectric + api.MeterCurrent + api.MeterEnergy + }{ + PCElectric: base, + MeterCurrent: &decoratePCEMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + MeterEnergy: &decoratePCEMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case meter != nil && meterCurrent != nil && meterEnergy != nil: + return &struct { + *PCElectric + api.Meter + api.MeterCurrent + api.MeterEnergy + }{ + PCElectric: base, + Meter: &decoratePCEMeterImpl{ + meter: meter, + }, + MeterCurrent: &decoratePCEMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + MeterEnergy: &decoratePCEMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + } + + return nil +} + +type decoratePCEMeterImpl struct { + meter func() (float64, error) +} + +func (impl *decoratePCEMeterImpl) CurrentPower() (float64, error) { + return impl.meter() +} + +type decoratePCEMeterCurrentImpl struct { + meterCurrent func() (float64, float64, float64, error) +} + +func (impl *decoratePCEMeterCurrentImpl) Currents() (float64, float64, float64, error) { + return impl.meterCurrent() +} + +type decoratePCEMeterEnergyImpl struct { + meterEnergy func() (float64, error) +} + +func (impl *decoratePCEMeterEnergyImpl) TotalEnergy() (float64, error) { + return impl.meterEnergy() +}