forked from evcc-io/evcc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rct.go
186 lines (155 loc) Β· 4.25 KB
/
rct.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package meter
import (
"errors"
"fmt"
"strings"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/mlnoga/rct"
)
/*
This meter supports devices implementing the RCT communication protocol, e.g. the RCT PS 6.0 with / without battery.
** Usages **
The following usages are supported:
- grid ... for reading the power imported or exported to the grid
- pv ... for reading the power produced by the pv
- battery ... for reading the power imported or exported to the battery
** Example configuration **
meters:
- name: GridMeter
type: rct
uri: 192.168.1.23
cache: 2s
usage: grid
- name: PvMeter
type: rct
uri: 192.168.1.23
cache: 2s
usage: pv
- name: BatteryMeter
type: rct
uri: 192.168.1.23
cache: 2s
usage: battery
*/
// RCT implements the api.Meter interface
type RCT struct {
bo *backoff.ExponentialBackOff
conn *rct.Connection // connection with the RCT device
usage string // grid, pv, battery
}
func init() {
registry.Add("rct", NewRCTFromConfig)
}
//go:generate go run ../cmd/tools/decorate.go -f decorateRCT -b *RCT -r api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryCapacity,Capacity,func() float64"
// NewRCTFromConfig creates an RCT from generic config
func NewRCTFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
capacity `mapstructure:",squash"`
Uri, Usage string
Cache time.Duration
}{
Cache: time.Second,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.Usage == "" {
return nil, errors.New("missing usage")
}
return NewRCT(cc.Uri, cc.Usage, cc.Cache, cc.capacity.Decorator())
}
// NewRCT creates an RCT meter
func NewRCT(uri, usage string, cache time.Duration, capacity func() float64) (api.Meter, error) {
conn, err := rct.NewConnection(uri, cache)
if err != nil {
return nil, err
}
bo := backoff.NewExponentialBackOff()
bo.InitialInterval = 10 * time.Millisecond
bo.MaxInterval = time.Second
m := &RCT{
usage: strings.ToLower(usage),
conn: conn,
bo: bo,
}
// decorate api.MeterEnergy
var totalEnergy func() (float64, error)
if usage == "grid" {
totalEnergy = m.totalEnergy
}
// decorate api.BatterySoc
var batterySoc func() (float64, error)
if usage == "battery" {
batterySoc = m.batterySoc
}
return decorateRCT(m, totalEnergy, batterySoc, capacity), nil
}
// CurrentPower implements the api.Meter interface
func (m *RCT) CurrentPower() (float64, error) {
switch m.usage {
case "grid":
res, err := m.queryFloat(rct.TotalGridPowerW)
return res, err
case "pv":
a, err := m.queryFloat(rct.SolarGenAPowerW)
if err != nil {
return 0, err
}
b, err := m.queryFloat(rct.SolarGenBPowerW)
if err != nil {
return 0, err
}
c, err := m.queryFloat(rct.S0ExternalPowerW)
return a + b + c, err
case "battery":
res, err := m.queryFloat(rct.BatteryPowerW)
return res, err
default:
return 0, fmt.Errorf("invalid usage: %s", m.usage)
}
}
// totalEnergy implements the api.MeterEnergy interface
func (m *RCT) totalEnergy() (float64, error) {
switch m.usage {
case "grid":
res, err := m.queryFloat(rct.TotalEnergyGridWh)
return res / 1000, err
case "pv":
a, err := m.queryFloat(rct.TotalEnergySolarGenAWh)
if err != nil {
return 0, err
}
b, err := m.queryFloat(rct.TotalEnergySolarGenBWh)
return (a + b) / 1000, err
case "battery":
in, err := m.queryFloat(rct.TotalEnergyBattInWh)
if err != nil {
return 0, err
}
out, err := m.queryFloat(rct.TotalEnergyBattOutWh)
return (in - out) / 1000, err
default:
return 0, fmt.Errorf("invalid usage: %s", m.usage)
}
}
// batterySoc implements the api.Battery interface
func (m *RCT) batterySoc() (float64, error) {
res, err := m.queryFloat(rct.BatterySoC)
return res * 100, err
}
// queryFloat adds retry logic of recoverable errors to QueryFloat32
func (m *RCT) queryFloat(id rct.Identifier) (float64, error) {
started := time.Now()
m.bo.Reset()
for {
next := m.bo.NextBackOff()
res, err := m.conn.QueryFloat32(id)
if err == nil || !errors.Is(err, &rct.RecoverableError{}) || time.Since(started)+next > m.bo.MaxElapsedTime {
return float64(res), err
}
time.Sleep(next)
}
}