forked from PaulSorenson/pyaurora
-
Notifications
You must be signed in to change notification settings - Fork 0
/
aurx.py
executable file
·200 lines (161 loc) · 6.47 KB
/
aurx.py
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
#!/usr/local/bin/python3.4
'''
Monitoring software for Aurora (now ABB) inverter via 'affable'
RS-485 to WiFi interface.
The kit came with a CD with windows software: Power 1 Aurora which
seems to work pretty well. Since I don't want to run windows 24/7
to monitor the PV it is not a monitoring option.
From curtronics.com we have open source aurora software written in C.
It expects a serial device and can be made to work indirectly using socat:
``socat PTY,link=$HOME/dev/solar,raw TCP4:192.168.1.140:8899``
A sample aurora command looks like so:
``aurora -a2 -A -Y8 -w25 -M5 -d0 -D -j -U 50 -s -t -n ~/dev/solar``
I found I had to use the -U option to specify a delay otherwise the reads
were unreliable.
I inspected the auroramon-1.8.8 code as well as running it in verbose mode
to inspect the data.
.. note::
The Wifi adapter will happily allow you to connect even if the
inverter is powered off (ie the sun not shining). So you need to
make a decision on how to deal with socket timeouts.
The model I used initially which may not be optimal but works is to
use ``circusd`` to respawn the process and included a backoff timer
when a socket timeout error occurs (the symptoms seen when the inverter
is offline).
.. moduleauthor:: paul sorenson
'''
import sys
import csv
import socket as skt
import zmq
import time
import datetime as dt
import functools as ft
from collections import OrderedDict
from argparse import ArgumentParser
import pyaurora as pv
from pyaurora.cozmq import tozmq
import logging
from logging.config import dictConfig
dictConfig(pv.logconfig)
log = logging.getLogger('aurora')
operations = (
#'getTime',
#'getFirmwareRel',
'gridPowerAll',
'powerPeakToday',
'dailyEnergy',
'weeklyEnergy',
#'last7Energy',
'partialEnergy',
'getEnergy10',
'frequencyAll',
'gridVoltageAll',
'gridVoltageAverage',
'gridCurrentAll',
'bulkVoltageDcDc',
'in1Voltage',
'in1Current',
'in2Voltage',
'in2Current',
'pin1All',
'pin2All',
'iLeakDcDc',
'iLeakInverter',
'boosterTemp',
)
'''The inverter operations to be polled in each cycle.'''
def mockinverterpoll(dictreader, operations, target):
'''
Return data from a file previously collected from a real
inverter. Note there is no attempt to match the time stamps
with real data, it will sequentially read through the file
provided.
:param dictreader: function that can issue commands and return
response. In general it accepts a command and subcommand (which
may be None).
:param operations: sequence of strings. The current implementation
looks up the relevant command, subcommand, decoder and formatter
to poll that value.
:param target: coroutine that accepts a dict (actually an ordered dict).
'''
now = dt.datetime.now()
log.debug('polling at {0}'.format(now))
od = OrderedDict()
utc = dt.datetime.utcnow()
od['utc'] = utc
data = next(dictreader)
for ssc in operations:
od[ssc] = data.get(ssc, None)
target.send(od)
def main():
a = ArgumentParser()
a.add_argument('--host', default='192.168.1.140',
help='WiFi adapter address (%(default)s).')
a.add_argument('--port', type=int, default=8899,
help='WiFi adapter port (%(default)s).')
a.add_argument('--inv-addr', type=int, default=2,
help='Inverter address (%(default)s)')
a.add_argument('--read-delay', type=float, default=0.05,
help='Time between command and read (%(default)f).')
a.add_argument('--connect-timeout', type=float, default=None,
help='Specify a timeout in seconds for connecting.')
a.add_argument('--default-timeout', type=float, default=5.0,
help='Set a default socket timeout in seconds (%(default)s).')
a.add_argument('--loop-interval', type=int, default=10,
help='''Time between inverter polls. If set to zero, the inverter
is polled a single time and the process exits (%(default)s).
Note that changing this may affect the relevance of some commands eg
"energy in last 10 seconds".''')
a.add_argument('--backoff', type=int, default=60,
help='''Sleep time after a socket timeout error (%(default)s). The
inverter goes offline each night and requests result in socket timeout and
subsequent process exit. This timeout is intended to reduce the frequency of
spawning processes during the night.''')
a.add_argument('--csv', help='''Optionally write CSV to file. The name
may contain `strftime` format strings. If the string is "stdout" the CSV
output will be directed to `sys.stdout`.''')
a.add_argument('--pub-url', default='tcp://127.0.0.1:8080',
help='''Specify a zeromq URL to publish JSON to (%(default)s).''')
a.add_argument('csv_in', nargs='?', default='aurora_2015-07-20.csv',
help='''Specify and input CSV file for testing (%(default)s).''')
opt = a.parse_args()
log.info('aurora starting')
log.debug(opt)
if opt.csv:
if opt.csv == 'stdout':
toutput = pv.tocsv(None)
else:
csvname = dt.datetime.now().strftime(opt.csv)
toutput = pv.tocsv(open(csvname, 'a'))
else:
if opt.pub_url:
context = zmq.Context()
zock = context.socket(zmq.PUB)
zock.bind(opt.pub_url)
sink = tozmq(zock)
else:
sink = pv.tostream()
toutput = pv.tojson(sink)
with skt.create_connection((opt.host, opt.port),
opt.connect_timeout) as sock:
if opt.default_timeout:
sock.settimeout(opt.default_timeout)
dictreader = csv.DictReader(open(opt.csv_in, 'r'))
try:
if opt.loop_interval:
pv.scheduler(opt.loop_interval, mockinverterpoll,
dictreader=dictreader, operations=operations,
target=toutput)
else:
mockinverterpoll(dictreader, operations, target=toutput)
except skt.timeout:
log.error('Socket timed out, application will exit')
if opt.backoff:
log.info('Backing off for {0} seconds.'.format(opt.backoff))
time.sleep(opt.backoff)
except KeyboardInterrupt:
log.warning('Ctrl-C received, application will exit')
log.info('aurora exiting')
if __name__ == '__main__':
main()