Skip to content

Commit 92730e4

Browse files
committed
feat: tinkerforge warp http api
1 parent 3115bb1 commit 92730e4

File tree

8 files changed

+1603
-0
lines changed

8 files changed

+1603
-0
lines changed

charger/warp-http.go

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
package charger
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"slices"
7+
"strings"
8+
9+
"github.com/evcc-io/evcc/api"
10+
"github.com/evcc-io/evcc/charger/warp"
11+
"github.com/evcc-io/evcc/util"
12+
"github.com/evcc-io/evcc/util/request"
13+
"github.com/jpfielding/go-http-digest/pkg/digest"
14+
)
15+
16+
// WarpHTTP is the Warp charger HTTP implementation
17+
type WarpHTTP struct {
18+
*request.Helper
19+
emHelper *request.Helper
20+
log *util.Logger
21+
uri string
22+
emURI string
23+
features []string
24+
current int64
25+
}
26+
27+
func init() {
28+
registry.Add("warp-http", NewWarpHTTPFromConfig)
29+
}
30+
31+
//go:generate go tool decorate -f decorateWarpHTTP -b *WarpHTTP -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.Identifier,Identify,func() (string, error)" -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)"
32+
33+
// NewWarpHTTPFromConfig creates a new configurable charger
34+
func NewWarpHTTPFromConfig(other map[string]any) (api.Charger, error) {
35+
cc := struct {
36+
URI string
37+
User string
38+
Password string
39+
EnergyManagerURI string
40+
EnergyManagerUser string
41+
EnergyManagerPassword string
42+
}{}
43+
44+
if err := util.DecodeOther(other, &cc); err != nil {
45+
return nil, err
46+
}
47+
48+
wb, err := NewWarpHTTP(cc.URI, cc.User, cc.Password, cc.EnergyManagerURI, cc.EnergyManagerUser, cc.EnergyManagerPassword)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
var currentPower, totalEnergy func() (float64, error)
54+
if wb.hasFeature(wb.uri, warp.FeatureMeter) {
55+
currentPower = wb.currentPower
56+
totalEnergy = wb.totalEnergy
57+
}
58+
59+
var currents, voltages func() (float64, float64, float64, error)
60+
if wb.hasFeature(wb.uri, warp.FeatureMeterPhases) {
61+
currents = wb.currents
62+
voltages = wb.voltages
63+
}
64+
65+
var identity func() (string, error)
66+
if wb.hasFeature(wb.uri, warp.FeatureNfc) {
67+
identity = wb.identify
68+
}
69+
70+
var phases func(int) error
71+
var getPhases func() (int, error)
72+
if cc.EnergyManagerURI != "" {
73+
if res, err := wb.emState(); err == nil && res.ExternalControl != 1 {
74+
phases = wb.phases1p3p
75+
getPhases = wb.getPhases
76+
}
77+
}
78+
79+
return decorateWarpHTTP(wb, currentPower, totalEnergy, currents, voltages, identity, phases, getPhases), nil
80+
}
81+
82+
// NewWarpHTTP creates a new configurable charger
83+
func NewWarpHTTP(uri, user, password, emURI, emUser, emPassword string) (*WarpHTTP, error) {
84+
log := util.NewLogger("warp-http")
85+
86+
client := request.NewHelper(log)
87+
client.Client.Timeout = warp.Timeout
88+
if user != "" {
89+
client.Client.Transport = digest.NewTransport(user, password, client.Client.Transport)
90+
}
91+
92+
wb := &WarpHTTP{
93+
Helper: client,
94+
log: log,
95+
uri: util.DefaultScheme(strings.TrimRight(uri, "/"), "http"),
96+
current: 6000, // mA
97+
}
98+
99+
if emURI != "" {
100+
wb.emURI = util.DefaultScheme(strings.TrimRight(emURI, "/"), "http")
101+
wb.emHelper = request.NewHelper(log)
102+
wb.emHelper.Client.Timeout = warp.Timeout
103+
if emUser != "" {
104+
wb.emHelper.Client.Transport = digest.NewTransport(emUser, emPassword, wb.emHelper.Client.Transport)
105+
}
106+
}
107+
108+
return wb, nil
109+
}
110+
111+
func (wb *WarpHTTP) hasFeature(root, feature string) bool {
112+
if wb.features == nil {
113+
var features []string
114+
uri := fmt.Sprintf("%s/info/features", root)
115+
116+
if err := wb.GetJSON(uri, &features); err == nil {
117+
wb.features = features
118+
119+
}
120+
}
121+
return slices.Contains(wb.features, feature)
122+
}
123+
124+
// Enable implements the api.Charger interface
125+
func (wb *WarpHTTP) Enable(enable bool) error {
126+
var current int64
127+
if enable {
128+
current = wb.current
129+
}
130+
return wb.setMaxCurrent(current)
131+
}
132+
133+
// Enabled implements the api.Charger interface
134+
func (wb *WarpHTTP) Enabled() (bool, error) {
135+
var res warp.EvseExternalCurrent
136+
uri := fmt.Sprintf("%s/evse/external_current", wb.uri)
137+
err := wb.GetJSON(uri, &res)
138+
return res.Current >= 6000, err
139+
}
140+
141+
// Status implements the api.Charger interface
142+
func (wb *WarpHTTP) Status() (api.ChargeStatus, error) {
143+
res := api.StatusNone
144+
145+
var status warp.EvseState
146+
uri := fmt.Sprintf("%s/evse/state", wb.uri)
147+
err := wb.GetJSON(uri, &status)
148+
if err != nil {
149+
return res, err
150+
}
151+
152+
switch status.Iec61851State {
153+
case 0:
154+
res = api.StatusA
155+
case 1:
156+
res = api.StatusB
157+
case 2:
158+
res = api.StatusC
159+
default:
160+
err = fmt.Errorf("invalid status: %d", status.Iec61851State)
161+
}
162+
163+
return res, err
164+
}
165+
166+
// MaxCurrent implements the api.Charger interface
167+
func (wb *WarpHTTP) MaxCurrent(current int64) error {
168+
return wb.MaxCurrentMillis(float64(current))
169+
}
170+
171+
var _ api.ChargerEx = (*WarpHTTP)(nil)
172+
173+
// MaxCurrentMillis implements the api.ChargerEx interface
174+
func (wb *WarpHTTP) MaxCurrentMillis(current float64) error {
175+
curr := int64(current * 1e3)
176+
err := wb.setMaxCurrent(curr)
177+
if err == nil {
178+
wb.current = curr
179+
}
180+
return err
181+
}
182+
183+
func (wb *WarpHTTP) setMaxCurrent(current int64) error {
184+
uri := fmt.Sprintf("%s/evse/external_current_update", wb.uri)
185+
data := map[string]int64{"current": current}
186+
187+
req, err := request.New(http.MethodPut, uri, request.MarshalJSON(data), request.JSONEncoding)
188+
if err != nil {
189+
return err
190+
}
191+
192+
var res interface{}
193+
return wb.DoJSON(req, &res)
194+
}
195+
196+
// CurrentPower implements the api.Meter interface
197+
func (wb *WarpHTTP) currentPower() (float64, error) {
198+
var res warp.MeterValues
199+
uri := fmt.Sprintf("%s/meter/values", wb.uri)
200+
err := wb.GetJSON(uri, &res)
201+
return res.Power, err
202+
}
203+
204+
// TotalEnergy implements the api.MeterEnergy interface
205+
func (wb *WarpHTTP) totalEnergy() (float64, error) {
206+
var res warp.MeterValues
207+
uri := fmt.Sprintf("%s/meter/values", wb.uri)
208+
err := wb.GetJSON(uri, &res)
209+
return res.EnergyAbs, err
210+
}
211+
212+
func (wb *WarpHTTP) meterValues() ([]float64, error) {
213+
var res []float64
214+
uri := fmt.Sprintf("%s/meter/all_values", wb.uri)
215+
err := wb.GetJSON(uri, &res)
216+
217+
if err == nil && len(res) < 6 {
218+
err = fmt.Errorf("invalid length: %d", len(res))
219+
}
220+
221+
return res, err
222+
}
223+
224+
// currents implements the api.MeterCurrrents interface
225+
func (wb *WarpHTTP) currents() (float64, float64, float64, error) {
226+
res, err := wb.meterValues()
227+
if err != nil {
228+
return 0, 0, 0, err
229+
}
230+
231+
return res[3], res[4], res[5], nil
232+
}
233+
234+
// voltages implements the api.MeterVoltages interface
235+
func (wb *WarpHTTP) voltages() (float64, float64, float64, error) {
236+
res, err := wb.meterValues()
237+
if err != nil {
238+
return 0, 0, 0, err
239+
}
240+
241+
return res[0], res[1], res[2], nil
242+
}
243+
244+
func (wb *WarpHTTP) identify() (string, error) {
245+
var res warp.ChargeTrackerCurrentCharge
246+
uri := fmt.Sprintf("%s/charge_tracker/current_charge", wb.uri)
247+
err := wb.GetJSON(uri, &res)
248+
return res.AuthorizationInfo.TagId, err
249+
}
250+
251+
func (wb *WarpHTTP) emState() (warp.EmState, error) {
252+
var res warp.EmState
253+
uri := fmt.Sprintf("%s/power_manager/state", wb.emURI)
254+
err := wb.emHelper.GetJSON(uri, &res)
255+
return res, err
256+
}
257+
258+
func (wb *WarpHTTP) emLowLevelState() (warp.EmLowLevelState, error) {
259+
var res warp.EmLowLevelState
260+
uri := fmt.Sprintf("%s/power_manager/low_level_state", wb.emURI)
261+
err := wb.emHelper.GetJSON(uri, &res)
262+
return res, err
263+
}
264+
265+
// phases1p3p implements the api.PhaseSwitcher interface
266+
func (wb *WarpHTTP) phases1p3p(phases int) error {
267+
res, err := wb.emState()
268+
if err != nil {
269+
return err
270+
}
271+
272+
if res.ExternalControl > 0 {
273+
return fmt.Errorf("external control not available: %s", res.ExternalControl.String())
274+
}
275+
276+
uri := fmt.Sprintf("%s/power_manager/external_control_update", wb.emURI)
277+
data := map[string]int{"phases_wanted": phases}
278+
279+
req, err := request.New(http.MethodPut, uri, request.MarshalJSON(data), request.JSONEncoding)
280+
if err != nil {
281+
return err
282+
}
283+
284+
var resp interface{}
285+
return wb.emHelper.DoJSON(req, &resp)
286+
}
287+
288+
// getPhases implements the api.PhaseGetter interface
289+
func (wb *WarpHTTP) getPhases() (int, error) {
290+
res, err := wb.emLowLevelState()
291+
if err != nil {
292+
return 0, err
293+
}
294+
295+
if res.Is3phase {
296+
return 3, nil
297+
}
298+
299+
return 1, nil
300+
}

0 commit comments

Comments
 (0)