Skip to content

Commit c1acc65

Browse files
committed
Initial
0 parents  commit c1acc65

File tree

3 files changed

+385
-0
lines changed

3 files changed

+385
-0
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Victron Faker
2+
This small program emulates the ET340 Energy Meter in a Victron ESS System. It reads
3+
values from an existing SMA Home Manager 2.0, and publishes the result on dbus as
4+
if it were the ET340 meter.
5+
6+
Use this at your own risk, I have no association with Victron or SMA and am providing
7+
this for anyone who already has these components and wants to play around with this.
8+
9+
I use this privately, and it works in my timezone, your results may vary
10+
11+
# Setup
12+
13+
First ensure that this will work: Try out https://github.com/mitchese/sma_home_manager_printer
14+
which will run on your Victron GX device and try to connect to the SMA meter. The above test
15+
program does _not_ publish its result on dbus for use by victron, only prints out the result
16+
for your verification. It should be relatively safe to test with.
17+
18+
If the `sma_home_manager_printer` works and shows consistent/reliable result, then you can
19+
install this in the same way. Download the latest release and copy.
20+
21+
While this is running, you should see correct values for a grid meter in your Venus UI:
22+
23+
![Venus GX UI](img/meter_sample.gif)
24+
25+
On the console of your GX device, you should see regular updates, around once per second:
26+
```
27+
root@victronvenusgx:~# ./shm-et340
28+
INFO[0000] Successfully connected to dbus and registered as a meter... Commencing reading of the SMA meter
29+
INFO[0000] Meter update received: 6677.15 kWh bought and 3200.45 kWh sold, 681.3 W currently flowing
30+
INFO[0001] Meter update received: 6677.15 kWh bought and 3200.45 kWh sold, 694.1 W currently flowing
31+
INFO[0002] Meter update received: 6677.15 kWh bought and 3200.45 kWh sold, 686.3 W currently flowing
32+
```
33+
34+
If this does not work, try to `export LOG_LEVEL="debug"` first, which should print out significantly more
35+
information on what's happening.
36+
37+
For more details, see the thread on the Victron Energy community forums here:
38+
39+
https://community.victronenergy.com/questions/49293/alternative-to-et340-mqtt-sma-home-manager.html
40+
41+
# TODO
42+
43+
- [ ] Setup a start/stop script and describe how to install as a system service
44+
- [ ] Make builds and releases automatic
45+
- [ ] Install and test with a real ESS / Multigrid
46+
- [ ] Test against fw upgrades of the Venus OS

img/meter_sample.gif

173 KB
Loading

shm-et340.go

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
package main
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
"net"
7+
"os"
8+
"strings"
9+
10+
"github.com/dmichael/go-multicast/multicast"
11+
"github.com/godbus/dbus"
12+
"github.com/godbus/dbus/introspect"
13+
log "github.com/sirupsen/logrus"
14+
)
15+
16+
const (
17+
address = "239.12.255.254:9522"
18+
)
19+
20+
var conn, err = dbus.ConnectSystemBus()
21+
22+
type singlePhase struct {
23+
voltage float32 // Volts: 230,0
24+
a float32 // Amps: 8,3
25+
power float32 // Watts: 1909
26+
forward float64 // kWh, purchased power
27+
reverse float64 // kWh, sold power
28+
}
29+
30+
const intro = `
31+
<node>
32+
<interface name="com.victronenergy.BusItem">
33+
<signal name="PropertiesChanged">
34+
<arg type="a{sv}" name="properties" />
35+
</signal>
36+
<method name="SetValue">
37+
<arg direction="in" type="v" name="value" />
38+
<arg direction="out" type="i" />
39+
</method>
40+
<method name="GetText">
41+
<arg direction="out" type="s" />
42+
</method>
43+
<method name="GetValue">
44+
<arg direction="out" type="v" />
45+
</method>
46+
</interface>` + introspect.IntrospectDataString + `</node> `
47+
48+
type objectpath string
49+
50+
var victronValues = map[int]map[objectpath]dbus.Variant{
51+
// 0: This will be used to store the VALUE variant
52+
0: map[objectpath]dbus.Variant{},
53+
// 1: This will be used to store the STRING variant
54+
1: map[objectpath]dbus.Variant{},
55+
}
56+
57+
func (f objectpath) GetValue() (dbus.Variant, *dbus.Error) {
58+
log.Debug("GetValue() called for ", f)
59+
log.Debug("...returning ", victronValues[0][f])
60+
return victronValues[0][f], nil
61+
}
62+
func (f objectpath) GetText() (string, *dbus.Error) {
63+
log.Debug("GetText() called for ", f)
64+
log.Debug("...returning ", victronValues[1][f])
65+
// Why does this end up ""SOMEVAL"" ... trim it I guess
66+
return strings.Trim(victronValues[1][f].String(), "\""), nil
67+
}
68+
69+
func init() {
70+
lvl, ok := os.LookupEnv("LOG_LEVEL")
71+
if !ok {
72+
lvl = "info"
73+
}
74+
75+
ll, err := log.ParseLevel(lvl)
76+
if err != nil {
77+
ll = log.DebugLevel
78+
}
79+
80+
log.SetLevel(ll)
81+
}
82+
83+
func main() {
84+
// Need to implement following paths:
85+
// https://github.com/victronenergy/venus/wiki/dbus#grid-meter
86+
// also in system.py
87+
victronValues[0]["/Connected"] = dbus.MakeVariant(1)
88+
victronValues[1]["/Connected"] = dbus.MakeVariant("1")
89+
90+
victronValues[0]["/CustomName"] = dbus.MakeVariant("Grid meter")
91+
victronValues[1]["/CustomName"] = dbus.MakeVariant("Grid meter")
92+
93+
victronValues[0]["/DeviceInstance"] = dbus.MakeVariant(30)
94+
victronValues[1]["/DeviceInstance"] = dbus.MakeVariant("30")
95+
96+
// also in system.py
97+
victronValues[0]["/DeviceType"] = dbus.MakeVariant(71)
98+
victronValues[1]["/DeviceType"] = dbus.MakeVariant("71")
99+
100+
victronValues[0]["/ErrorCode"] = dbus.MakeVariantWithSignature(0, dbus.SignatureOf(123))
101+
victronValues[1]["/ErrorCode"] = dbus.MakeVariant("0")
102+
103+
victronValues[0]["/FirmwareVersion"] = dbus.MakeVariant(2)
104+
victronValues[1]["/FirmwareVersion"] = dbus.MakeVariant("2")
105+
106+
// also in system.py
107+
victronValues[0]["/Mgmt/Connection"] = dbus.MakeVariant("/dev/ttyUSB0")
108+
victronValues[1]["/Mgmt/Connection"] = dbus.MakeVariant("/dev/ttyUSB0")
109+
110+
victronValues[0]["/Mgmt/ProcessName"] = dbus.MakeVariant("/opt/color-control/dbus-cgwacs/dbus-cgwacs")
111+
victronValues[1]["/Mgmt/ProcessName"] = dbus.MakeVariant("/opt/color-control/dbus-cgwacs/dbus-cgwacs")
112+
113+
victronValues[0]["/Mgmt/ProcessVersion"] = dbus.MakeVariant("1.8.0")
114+
victronValues[1]["/Mgmt/ProcessVersion"] = dbus.MakeVariant("1.8.0")
115+
116+
victronValues[0]["/Position"] = dbus.MakeVariantWithSignature(0, dbus.SignatureOf(123))
117+
victronValues[1]["/Position"] = dbus.MakeVariant("0")
118+
119+
// also in system.py
120+
victronValues[0]["/ProductId"] = dbus.MakeVariant(45058)
121+
victronValues[1]["/ProductId"] = dbus.MakeVariant("45058")
122+
123+
// also in system.py
124+
victronValues[0]["/ProductName"] = dbus.MakeVariant("Grid meter")
125+
victronValues[1]["/ProductName"] = dbus.MakeVariant("Grid meter")
126+
127+
victronValues[0]["/Serial"] = dbus.MakeVariant("BP98305081235")
128+
victronValues[1]["/Serial"] = dbus.MakeVariant("BP98305081235")
129+
130+
// Provide some initial values... note that the values must be a valid formt otherwise dbus_systemcalc.py exits like this:
131+
//@400000005ecc11bf3782b374 File "/opt/victronenergy/dbus-systemcalc-py/dbus_systemcalc.py", line 386, in _handletimertick
132+
//@400000005ecc11bf37aa251c self._updatevalues()
133+
//@400000005ecc11bf380e74cc File "/opt/victronenergy/dbus-systemcalc-py/dbus_systemcalc.py", line 678, in _updatevalues
134+
//@400000005ecc11bf383ab4ec c = _safeadd(c, p, pvpower)
135+
//@400000005ecc11bf386c9674 File "/opt/victronenergy/dbus-systemcalc-py/sc_utils.py", line 13, in safeadd
136+
//@400000005ecc11bf387b28ec return sum(values) if values else None
137+
//@400000005ecc11bf38b2bb7c TypeError: unsupported operand type(s) for +: 'int' and 'unicode'
138+
//
139+
victronValues[0]["/Ac/L1/Power"] = dbus.MakeVariant(0.0)
140+
victronValues[1]["/Ac/L1/Power"] = dbus.MakeVariant("0 W")
141+
victronValues[0]["/Ac/L2/Power"] = dbus.MakeVariant(0.0)
142+
victronValues[1]["/Ac/L2/Power"] = dbus.MakeVariant("0 W")
143+
victronValues[0]["/Ac/L3/Power"] = dbus.MakeVariant(0.0)
144+
victronValues[1]["/Ac/L3/Power"] = dbus.MakeVariant("0 W")
145+
146+
victronValues[0]["/Ac/L1/Voltage"] = dbus.MakeVariant(230)
147+
victronValues[1]["/Ac/L1/Voltage"] = dbus.MakeVariant("230 V")
148+
victronValues[0]["/Ac/L2/Voltage"] = dbus.MakeVariant(230)
149+
victronValues[1]["/Ac/L2/Voltage"] = dbus.MakeVariant("230 V")
150+
victronValues[0]["/Ac/L3/Voltage"] = dbus.MakeVariant(230)
151+
victronValues[1]["/Ac/L3/Voltage"] = dbus.MakeVariant("230 V")
152+
153+
victronValues[0]["/Ac/L1/Current"] = dbus.MakeVariant(0.0)
154+
victronValues[1]["/Ac/L1/Current"] = dbus.MakeVariant("0 A")
155+
victronValues[0]["/Ac/L2/Current"] = dbus.MakeVariant(0.0)
156+
victronValues[1]["/Ac/L2/Current"] = dbus.MakeVariant("0 A")
157+
victronValues[0]["/Ac/L3/Current"] = dbus.MakeVariant(0.0)
158+
victronValues[1]["/Ac/L3/Current"] = dbus.MakeVariant("0 A")
159+
160+
victronValues[0]["/Ac/L1/Energy/Forward"] = dbus.MakeVariant(0.0)
161+
victronValues[1]["/Ac/L1/Energy/Forward"] = dbus.MakeVariant("0 kWh")
162+
victronValues[0]["/Ac/L2/Energy/Forward"] = dbus.MakeVariant(0.0)
163+
victronValues[1]["/Ac/L2/Energy/Forward"] = dbus.MakeVariant("0 kWh")
164+
victronValues[0]["/Ac/L3/Energy/Forward"] = dbus.MakeVariant(0.0)
165+
victronValues[1]["/Ac/L3/Energy/Forward"] = dbus.MakeVariant("0 kWh")
166+
167+
victronValues[0]["/Ac/L1/Energy/Reverse"] = dbus.MakeVariant(0.0)
168+
victronValues[1]["/Ac/L1/Energy/Reverse"] = dbus.MakeVariant("0 kWh")
169+
victronValues[0]["/Ac/L2/Energy/Reverse"] = dbus.MakeVariant(0.0)
170+
victronValues[1]["/Ac/L2/Energy/Reverse"] = dbus.MakeVariant("0 kWh")
171+
victronValues[0]["/Ac/L3/Energy/Reverse"] = dbus.MakeVariant(0.0)
172+
victronValues[1]["/Ac/L3/Energy/Reverse"] = dbus.MakeVariant("0 kWh")
173+
174+
basicPaths := []dbus.ObjectPath{
175+
"/Connected",
176+
"/CustomName",
177+
"/DeviceInstance",
178+
"/DeviceType",
179+
"/ErrorCode",
180+
"/FirmwareVersion",
181+
"/Mgmt/Connection",
182+
"/Mgmt/ProcessName",
183+
"/Mgmt/ProcessVersion",
184+
"/Position",
185+
"/ProductId",
186+
"/ProductName",
187+
"/Serial",
188+
}
189+
190+
updatingPaths := []dbus.ObjectPath{
191+
"/Ac/L1/Power",
192+
"/Ac/L2/Power",
193+
"/Ac/L3/Power",
194+
"/Ac/L1/Voltage",
195+
"/Ac/L2/Voltage",
196+
"/Ac/L3/Voltage",
197+
"/Ac/L1/Current",
198+
"/Ac/L2/Current",
199+
"/Ac/L3/Current",
200+
"/Ac/L1/Energy/Forward",
201+
"/Ac/L2/Energy/Forward",
202+
"/Ac/L3/Energy/Forward",
203+
"/Ac/L1/Energy/Reverse",
204+
"/Ac/L2/Energy/Reverse",
205+
"/Ac/L3/Energy/Reverse",
206+
}
207+
208+
defer conn.Close()
209+
210+
// Some of the victron stuff requires it be called grid.cgwacs... using the only known valid value (from the simulator)
211+
// This can _probably_ be changed as long as it matches com.victronenergy.grid.cgwacs_*
212+
reply, err := conn.RequestName("com.victronenergy.grid.cgwacs_ttyUSB0_di30_mb1",
213+
dbus.NameFlagDoNotQueue)
214+
if err != nil {
215+
log.Panic("Something went horribly wrong in the dbus connection")
216+
panic(err)
217+
}
218+
219+
if reply != dbus.RequestNameReplyPrimaryOwner {
220+
log.Panic("name cgwacs_ttyUSB0_di30_mb1 already taken on dbus.")
221+
os.Exit(1)
222+
}
223+
224+
for i, s := range basicPaths {
225+
log.Debug("Registering dbus basic path #", i, ": ", s)
226+
conn.Export(objectpath(s), s, "com.victronenergy.BusItem")
227+
conn.Export(introspect.Introspectable(intro), s, "org.freedesktop.DBus.Introspectable")
228+
}
229+
230+
for i, s := range updatingPaths {
231+
log.Debug("Registering dbus update path #", i, ": ", s)
232+
conn.Export(objectpath(s), s, "com.victronenergy.BusItem")
233+
conn.Export(introspect.Introspectable(intro), s, "org.freedesktop.DBus.Introspectable")
234+
}
235+
236+
log.Info("Successfully connected to dbus and registered as a meter... Commencing reading of the SMA meter")
237+
238+
multicast.Listen(address, msgHandler)
239+
// This is a forever loop^^
240+
panic("Error: We terminated.... how did we ever get here?")
241+
}
242+
243+
func msgHandler(src *net.UDPAddr, n int, b []byte) {
244+
// This function will be called with every datagram sent by the SMA meter
245+
246+
// 0-28: SMA/SUSyID/SN/Uptime
247+
log.Debug("----------------------")
248+
log.Debug("Received datagram from meter")
249+
log.Debug("Uid: ", binary.BigEndian.Uint32(b[4:8]))
250+
log.Debug("Serial: ", binary.BigEndian.Uint32(b[20:24]))
251+
252+
// ...buy.... ...sell... both in 0.1W, converted to W
253+
powertot := ((float32(binary.BigEndian.Uint32(b[32:36])) - float32(binary.BigEndian.Uint32(b[52:56]))) / 10.0)
254+
255+
// in watt seconds, convert to kWh
256+
bezugtot := float64(binary.BigEndian.Uint64(b[40:48])) / 3600.0 / 1000.0
257+
einsptot := float64(binary.BigEndian.Uint64(b[60:68])) / 3600.0 / 1000.0
258+
259+
log.Debug("Total W: ", powertot)
260+
log.Debug("Total Buy kWh: ", bezugtot)
261+
log.Debug("Total Sell kWh: ", einsptot)
262+
263+
log.Info(fmt.Sprintf("Meter update received: %.2f kWh bought and %.2f kWh sold, %.1f W currently flowing", bezugtot, einsptot, powertot))
264+
updateVariant(float64(powertot), "W", "/Ac/Power")
265+
updateVariant(float64(einsptot), "kWh", "/Ac/Energy/Reverse")
266+
updateVariant(float64(bezugtot), "kWh", "/Ac/Energy/Forward")
267+
268+
L1 := decodePhaseChunk(b[164:308])
269+
L2 := decodePhaseChunk(b[308:452])
270+
L3 := decodePhaseChunk(b[452:596])
271+
272+
log.Debug("+-----+-------------+---------------+---------------+")
273+
log.Debug("|value| L1 \t| L2 \t| L3 \t|")
274+
log.Debug("+-----+-------------+---------------+---------------+")
275+
log.Debug(fmt.Sprintf("| V | %8.2f \t| %8.2f \t| %8.2f \t|", L1.voltage, L2.voltage, L3.voltage))
276+
log.Debug(fmt.Sprintf("| A | %8.2f \t| %8.2f \t| %8.2f \t|", L1.a, L2.a, L3.a))
277+
log.Debug(fmt.Sprintf("| W | %8.2f \t| %8.2f \t| %8.2f \t|", L1.power, L2.power, L3.power))
278+
log.Debug(fmt.Sprintf("| kWh | %8.2f \t| %8.2f \t| %8.2f \t|", L1.forward, L2.forward, L3.forward))
279+
log.Debug(fmt.Sprintf("| kWh | %8.2f \t| %8.2f \t| %8.2f \t|", L1.reverse, L2.reverse, L3.reverse))
280+
log.Debug("+-----+-------------+---------------+---------------+")
281+
282+
// L1
283+
updateVariant(float64(L1.power), "W", "/Ac/L1/Power")
284+
updateVariant(float64(L1.voltage), "V", "/Ac/L1/Voltage")
285+
updateVariant(float64(L1.a), "A", "/Ac/L1/Current")
286+
updateVariant(L1.forward, "kWh", "/Ac/L1/Energy/Forward")
287+
updateVariant(L1.reverse, "kWh", "/Ac/L1/Energy/Reverse")
288+
289+
// L2
290+
updateVariant(float64(L2.power), "W", "/Ac/L2/Power")
291+
updateVariant(float64(L2.voltage), "V", "/Ac/L2/Voltage")
292+
updateVariant(float64(L2.a), "A", "/Ac/L2/Current")
293+
updateVariant(L2.forward, "kWh", "/Ac/L2/Energy/Forward")
294+
updateVariant(L2.reverse, "kWh", "/Ac/L2/Energy/Reverse")
295+
296+
// L3
297+
updateVariant(float64(L3.power), "W", "/Ac/L3/Power")
298+
updateVariant(float64(L3.voltage), "V", "/Ac/L3/Voltage")
299+
updateVariant(float64(L3.a), "A", "/Ac/L3/Current")
300+
updateVariant(L3.forward, "kWh", "/Ac/L3/Energy/Forward")
301+
updateVariant(L3.reverse, "kWh", "/Ac/L3/Energy/Reverse")
302+
303+
}
304+
305+
func decodePhaseChunk(b []byte) *singlePhase {
306+
307+
// why does this measure in 1/10 of watts?!
308+
bezugW := float32(binary.BigEndian.Uint32(b[4:8])) / 10.0
309+
einspeiseW := float32(binary.BigEndian.Uint32(b[24:28])) / 10.0
310+
311+
// this is in watt seconds ... chagne to kilo(100)watthour(3600)s:
312+
bezugkWh := float64(binary.BigEndian.Uint64(b[12:20])) / 3600.0 / 1000.0
313+
einspeisekWh := float64(binary.BigEndian.Uint64(b[32:40])) / 3600.0 / 1000.0
314+
315+
// not used, but leaving here for future
316+
//bezugVA := float32(binary.BigEndian.Uint32(b[84:88])) / 10
317+
//einspeiseVA := float32(binary.BigEndian.Uint32(b[104:108])) / 10
318+
319+
L := singlePhase{}
320+
L.voltage = float32(binary.BigEndian.Uint32(b[132:136])) / 1000 // millivolts!
321+
L.power = bezugW - einspeiseW
322+
L.a = L.power / L.voltage
323+
L.forward = bezugkWh
324+
L.reverse = einspeisekWh
325+
326+
return &L
327+
//log.Println(phase, "Buy: ", float32(binary.BigEndian.Uint32(b[4:8]))/10)
328+
//log.Println(phase, "Sell: ", float32(binary.BigEndian.Uint32(b[24:28]))/10)
329+
//return
330+
}
331+
332+
func updateVariant(value float64, unit string, path string) {
333+
emit := make(map[string]dbus.Variant)
334+
emit["Text"] = dbus.MakeVariant(fmt.Sprintf("%.2f", value) + unit)
335+
emit["Value"] = dbus.MakeVariant(float64(value))
336+
victronValues[0][objectpath(path)] = emit["Value"]
337+
victronValues[1][objectpath(path)] = emit["Text"]
338+
conn.Emit(dbus.ObjectPath(path), "com.victronenergy.BusItem.PropertiesChanged", emit)
339+
}

0 commit comments

Comments
 (0)