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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
| import crcmod, struct, socket, datetime, sys, traceback, requests, json
import MySQLdb as mdb
from daemon import Daemon
from time import sleep
from time import time
TCP_IP = '192.168.0.30'
TCP_PORT = 23
INVERTER_ADDR = 3
crc16 = crcmod.mkCrcFun(0x18005, 0xffff, True)
class Request:
"""abstract class for building requests"""
def __init__(self, address, socket):
self._address = struct.pack('B',address)
self._socket = socket
"""create modbus request with function code 4"""
self._request = bytearray(b'\x00\x04\x00\x00\x00\x00')
self._request[0] = self._address
def send(self):
#calculate and append checksum
tmp_request = bytearray()
tmp_request = self._request + struct.pack("<H", crc16(bytes(self._request)))
print("send")
print(":".join("{:02x}".format(c) for c in tmp_request))
self._socket.sendall(tmp_request)
def EToday(self):
#Mem addr 2, Type U32, Unit kwh, Gain 0.1
self._request[3]=b'\x02'
self._request[5]=b'\x02'
self.send()
response = self.read('U32')
if len(response)==0:
return
return tuple(0.1*x for x in response) #kWh
def ETotal(self):
#Mem addr 4, Type U32, Unit kWh, Gain 0.1
self._request[3]=b'\x04'
self._request[5]=b'\x02'
self.send()
response = self.read('U32')
if len(response)==0:
return
return tuple(0.1*x for x in response) #kWh
def HTotal(self):
#Mem addr 6, Type U32, Unit h, Gain 1
self._request[3]=b'\x06'
self._request[5]=b'\x02'
self.send()
response = self.read('U32')
if len(response)==0:
return
return response # hours
def Pac(self):
#Mem addr 38, Type U32, Unit W, Gain 1
self._request[3]=b'\x26'
self._request[5]=b'\x02'
self.send()
response = self.read('U32')
if len(response)==0:
return
return response # Watt
def Pdc(self):
#Mem addr 18-1, Type U16 *4, Unit V,A , Gain 0.1
self._request[3]=b'\x11'
self._request[5]=b'\x04'
self.send()
response = self.read('U16')
if len(response)==0:
return
return tuple(0.1*x for x in response) #PV1 V, PV1 A, PV2 V, PV2 A
def Temp(self):
#Mem addr 15-1, Type S16, Unit C , Gain 0.1
self._request[3]=b'\x0E'
self._request[5]=b'\x01'
self.send()
response = self.read('S16')
if len(response)==0:
return
return tuple(0.1*x for x in response) #Temp in Celcius
def read(self,type):
"""Call this after sending a request. It will return an list of response objects, if any."""
rss = () #response list
data = "" #socket data
while (1):
try:
part = self._socket.recv(2) #gather all data untill timeout
data += part
except:
break
if (len(data) == 0):
# No response, problably the inverter is not running because it is dark outside
return rss # empty
#check CRC
print("received:")
print(":".join("{:02x}".format(ord(c)) for c in data))
if data[-2:] != struct.pack("<H",crc16(bytes(data[:-2]))):
#probably missing inverter address in response
#add address and check CRC again
if data[-2:] != struct.pack("<H",crc16(bytes(self._address + data[:-2]))):
#CRC incorrect
print("Incorrect checksum")
return rss #empty
data = self._address + data #add the address to make it right
#since CRC is correct we can safely assume 1st byte is the inverter address, 2nd function code and 3rd the length.
#strip crc and first 3 bytes. Endian swap and interpret depending on the type.
print(":".join("{:02x}".format(ord(c)) for c in data))
print("data2: ", data[2])
if type=='U16': #unsigned short
for x in struct.unpack("H"*(struct.unpack("B",data[2])[0]//2),data[3:-2]):
rss += struct.unpack("H",struct.pack(">H",x))
return rss
elif type=='U32': #unsigned int
tmp = ""
for x in struct.unpack("H"*(struct.unpack("B",data[2])[0]//2),data[3:-2]):
tmp += struct.pack(">H",x)
print(len(tmp))
for y in struct.unpack("HH"*(len(tmp)//4),tmp):
rss += struct.unpack("I",struct.pack("<I",y))
return rss
elif type=='S16': #signed short
for x in struct.unpack("h"*(struct.unpack("B",data[2])[0]//2),data[3:-2]):
rss += struct.unpack("h",struct.pack(">h",x))
return rss
else:
Debug("unknown type")
def Debug(mess):
now = datetime.datetime.now()
now = now.strftime("%Y-%m-%d %H:%M:%S")
with open("/home/pi/Zeversolar_errors.log", "a") as file:
file.write(now + ": " + mess + "\r\n")
print now + ": " + mess + "\r\n"
def full_read_to_db(r):
try: #put everything in a big try, in case something goes wrong, logging must go on.
sleep(0.003) #get minimal 28 bits silence before transmit
EToday=None
ETotal=None
HTotal=None
Pac=None
Pdc=None
Temp=None
retries = 0
while EToday == None and retries<3:
tmp = r.EToday()
if tmp!=None:
EToday = tmp[0]
retries=0 #request succesfull reset retries for next request
else:
retries+=1 #request unsuccessfull increase retries
sleep(0.3) #give inverter a break and get minimal 28 bits silence before retry
while ETotal == None and retries<3:
tmp = r.ETotal()
if tmp!=None:
ETotal = tmp[0]
retries=0
else:
retries+=1
sleep(0.3) #get minimal 28 bits silence before retry
while HTotal == None and retries<3:
tmp = r.HTotal()
if tmp!=None:
HTotal = tmp[0]
retries=0
else:
retries+=1
sleep(0.3) #get minimal 28 bits silence before retry
while Pac == None and retries<3:
tmp = r.Pac()
if tmp != None:
Pac=tmp[0]
retries=0
else:
retries+=1
sleep(0.3)
while Pdc == None and retries<3:
tmp = r.Pdc() #remains tuple of V1,A1,V2,A2
if tmp != None:
Pdc=tmp
retries=0
else:
retries+=1
sleep(0.3)
while Temp == None and retries<3:
tmp = r.Temp()
if tmp != None:
Temp=tmp[0]
retries=0
else:
retries+=1
sleep(0.3)
# s.close()
# r=None #cleanup
if EToday != None and ETotal != None and HTotal != None and Pac != None and Pdc != None and Temp != None:
#skip db write in case a value is missing
print("EToday(kWh): " + str(EToday) + "\n" +\
"ETotal(kWh): " + str(ETotal) + "\n" +\
"HTotal(h): " + str(HTotal) + "\n" + \
"Pac(W): " + str(Pac) + "\n" +\
"Pdc PV1(W): " + str(Pdc[0]*Pdc[1]) + "\n" +\
"Pdc PV2(W): " + str(Pdc[2]*Pdc[3]) + "\n" +\
"Pdc(W): " + str(Pdc[0]*Pdc[1]+Pdc[2]*Pdc[3]) + "\n" +\
"Temp(C): " + str(Temp))
#At startup of the inverter there is a glitch where DC power can be 8kW.
if Pdc[0]*Pdc[1] >Pac or Pdc[2]*Pdc[3] > Pac:
Pdc = (Pdc[0],'0',Pdc[2],'0')
con = None
try:
con = mdb.connect('localhost', 'solar', 'xxx', 'Energy');
cur = con.cursor()
cur.execute("INSERT INTO `solar`(`Vdc1`, `Idc1`, `Vdc2`, `Idc2`, `Pac`, `EToday`, `Etotal`,`HTotal`) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)",(Pdc[0],Pdc[1],Pdc[2],Pdc[3],Pac,EToday,ETotal,HTotal))
con.commit()
except mdb.Error, e:
Debug("Error %d: %s"% (e.args[0],e.args[1]))
finally:
if con:
con.close()
except:
Debug("Something went wrong... Traceback: "+traceback.format_tb(sys.exc_info()[2])[0] + "\nError Info:\n" + str(sys.exc_info()[1]))
def lite_read_to_curl(r):
try: #put everything in a big try, in case something goes wrong, logging must go on.
sleep(0.003) #get minimal 28 bits silence before transmit
Pac=None
retries = 0
while Pac == None and retries<3:
tmp = r.Pac()
if tmp != None:
Pac=tmp[0]
retries=0
else:
retries+=1
sleep(0.3)
if Pac != None:
try:
response = requests.post('http://localhost:8123/api/states/sensor.sunpower', headers={'x-ha-access': 'xxxxxxxxxx', 'content-type': 'application/json'}, data=json.dumps({'state': Pac, 'attributes': {"unit_of_measurement": "Watt",'friendly_name': 'SunPower'}}))
print(response.text)
except:
Debug("Something went wrong... Traceback: "+traceback.format_tb(sys.exc_info()[2])[0] + "\nError Info:\n" + str(sys.exc_info()[1]))
pass
except:
Debug("Something went wrong... Traceback: "+traceback.format_tb(sys.exc_info()[2])[0] + "\nError Info:\n" + str(sys.exc_info()[1]))
class MyDaemon(Daemon):
def run (self):
while True:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
try:
s.connect((TCP_IP, TCP_PORT)) # connect to tcp-converter
except:
Debug("Unable to connect to tcp converter - ")
r = Request(INVERTER_ADDR,s)
if round(time(),0)%60 == 0:
full_read_to_db(r)
lite_read_to_curl(r)
s.close()
r=None #cleanup
sleep(2.0-(time()%2.0))
if __name__ == "__main__":
daemon = MyDaemon('/tmp/solarlogger.pid')
if len(sys.argv) == 2:
if 'start' == sys.argv[1]:
daemon.start()
elif 'stop' == sys.argv[1]:
daemon.stop()
elif 'restart' == sys.argv[1]:
daemon.restart()
elif 'run' == sys.argv[1]:
daemon.run()
else:
print "Unknown command"
sys.exit(2)
sys.exit(0)
else:
print "usage: %s start|stop|restart|run" % sys.argv[0]
sys.exit(2) |